C.A.S (Chess Assistance System)

Enhance your chess performance with a cutting-edge real-time move analysis and strategy assistance system

目前為 2023-02-16 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        C.A.S (Chess Assistance System)
// @namespace   sayfpack
// @author      sayfpack
// @version     1.5
// @homepageURL https://github.com/Hakorr/Userscripts/tree/main/Other/A.C.A.S
// @supportURL  https://github.com/Hakorr/Userscripts/issues/new
// @match       https://www.chess.com/*
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_xmlhttpRequest
// @grant       GM_getResourceText
// @grant       GM_registerMenuCommand
// @description Enhance your chess performance with a cutting-edge real-time move analysis and strategy assistance system
// @require     https://greasyfork.org/scripts/459136-usergui/code/UserGui.js?version=1143683
// @resource    jquery.js       https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
// @resource    chessboard.js   https://raw.githubusercontent.com/Hakorr/Userscripts/main/Other/A.C.A.S/content/chessboard.js
// @resource    chessboard.css  https://raw.githubusercontent.com/Hakorr/Userscripts/main/Other/A.C.A.S/content/chessboard.css
// @resource    lozza.js        https://raw.githubusercontent.com/Hakorr/Userscripts/main/Other/A.C.A.S/content/lozza.js
// @resource    lozza2.js       https://raw.githubusercontent.com/op12no2/lozza/master/history/2.6/lozza.js
// @resource    stockfish.js    https://github.com/exoticorn/stockfish-js/releases/download/sf_5_js/stockfish.js
// @resource    stockfish2.js   https://github.com/lichess-org/stockfish.js/releases/download/ddugovic-250718/stockfish.js
// @run-at      document-start
// @inject-into content
// ==/UserScript==

/*
     e            e88~-_            e           ,d88~~\
    d8b          d888   \          d8b          8888
   /Y88b         8888             /Y88b         `Y88b
  /  Y88b        8888            /  Y88b         `Y88b,
 /____Y88b  d88b Y888   / d88b  /____Y88b  d88b    8888
/      Y88b Y88P  "88_-~  Y88P /      Y88b Y88P \__88P'

Advanced Chess Assistance System (A.C.A.S) v1 | Q1 2023

[WARNING]
- Please be advised that the use of A.C.A.S may violate the rules and lead to disqualification or banning from tournaments and online platforms.
- The developers of A.C.A.S and related systems will NOT be held accountable for any consequences resulting from its use.
- We strongly advise to use A.C.A.S only in a controlled environment ethically.*/

// DANGER ZONE - DO NOT PROCEED IF YOU DON'T KNOW WHAT YOU'RE DOING //
/*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*/
//////////////////////////////////////////////////////////////////////
// DANGER ZONE - DO NOT PROCEED IF YOU DON'T KNOW WHAT YOU'RE DOING //
const ANALYS_API_URL="https://lichess.org/api/cloud-eval";
const TABLEBASE_API_URL="https://tablebase.lichess.ovh/standard";
const USE_API_LICHESS=false
const SEND_MOVES_TO_CHESSAPP=false;

let engineObjectURL2
let lichess_worked=false;

let pgn='';

function moveResult(from,to,power){
	removeSiteMoveMarkings();
	Interface.boardUtils.removeBestMarkings();
	
	const isPlayerTurn = playerColor == turn;
	let enemyScore=0;
	let myScore=0;

	if(isPlayerTurn) // my turn
		myScore=myScore+Number(power);
	else
		enemyScore=enemyScore+Number(power);

	Interface.boardUtils.updateBoardPower(myScore,enemyScore);
			
	if(GM_getValue(dbValues.displayMovesOnSite)) {
		markMoveToSite(from, to, isPlayerTurn);
	}



	Interface.boardUtils.markMove(from, to, isPlayerTurn);
	Interface.stopBestMoveProcessingAnimation();


}

function getRandomMove(fen){
	GM_xmlhttpRequest ({
        method:     "GET",
        url:        TABLEBASE_API_URL+"?fen="+fen,
        headers:    {
            "Content-Type": "application/json"
        },
        onload:     function (response) {
           
			let data=JSON.parse(response.response);
			let moves=data.moves;
			let random_int=Math.floor(Math.random() * moves.length);
			let nextMove=data.moves[random_int].uci;

			
			const [from, to] = [nextMove.slice(0,2),nextMove.slice(2,4)];
			
			moveResult(from,to,0);
			
			

        }
    });
}
function getPGN ()  {
		let pgn='';
		for (const el of Array.from(document.querySelectorAll('vertical-move-list > .move'))) {
			const number = el.dataset.wholeMoveNumber

			pgn += `${number}. `

		for (const node of Array.from(el.querySelectorAll('.node'))) {
			const figurine = node.querySelector('span')

			if (figurine) {
				pgn += `${figurine.dataset.figurine}`
			}

			pgn += `${node.innerText} `
		}
		}
	return pgn;
}


function getBestMoves2(fen,depth="",movetime=""){


	GM_xmlhttpRequest ({
        method:     "GET",
        url:        "http://localhost:5000/getBestMove?fen="+fen+"&depth="+depth+"&movetime="+movetime,
        headers:    {
            "Content-Type": "application/json"
        },
        onload:     function (response) {
			let data=JSON.parse(response.response);
			let depth=data.depth;
			let power=data.score;
			let nextMove=data.move;
			
			
			const [from,to] = [nextMove.slice(0,2),nextMove.slice(2,4)];
			
			moveResult(from,to,power);
			
        },onerror:	function(error){
			console.log("check api server");
			console.log("using local engine !!");
			loadRandomChessengine(fen);
				
		}
    });

}


function getBestMoves(fen){

	GM_xmlhttpRequest ({
        method:     "GET",
        url:        ANALYS_API_URL+"?fen="+fen+"&variant=fromPosition",
        headers:    {
            "Content-Type": "application/json"
        },
        onload:     function (response) {
           
			
			if(response.responseText.includes("error")){
				lichess_worked=false;
				if(USE_API_LICHESS){
					console.log("using lichess api engine !!");
					if(playerColor == null || turn == playerColor)
						getBestMoves2(fen);
				}else{
					console.log("using local engine !!");
					loadRandomChessengine(fen);
				}
				

				
			}else {
				lichess_worked=true;
				console.log("using lichess engine !!");

				
				
			
				let data=JSON.parse(response.response);
				let move=((data.pvs[0]).moves).split(' ');
				let power=((data.pvs[0]).cp);
				let nextMove=move[0];
			
				const [from, to] = [nextMove.slice(0,2),nextMove.slice(2,4)];
			
				moveResult(from,to,power);
			}
        }
    });
}


function sendMoves(){
	const black = document.querySelector('chess-board')?.classList.contains('flipped') || false

	
	pgn=getPGN();

	GM_xmlhttpRequest ({
        method:     "POST",
        url:        "http://localhost:30012",
		data:		JSON.stringify({black,pgn}),
        headers:    {
            "Content-Type": "application/json"
        },
        onload:     function (response) {
			
        }
    });
}


const reload_every=40;
const enableLog=1;

const repositoryRawURL = 'https://raw.githubusercontent.com/Hakorr/Userscripts/main/Other/A.C.A.S';
const repositoryURL = 'https://github.com/Hakorr/Userscripts/tree/main/Other/A.C.A.S';

let engineIndex=4;

const dbValues = {
    engineDepthQuery: 'engineDepthQuery',
    displayMovesOnSite: 'displayMovesOnSite',
    openGuiAutomatically: 'openGuiAutomatically',
    subtleMode: 'subtleMode',
    subtletiness: 'subtletiness'
};

let reload_count=0;



let Interface = null;
let LozzaUtils = null;

let initialized = false;
let firstMoveMade = false;


let engine = null;
let engine2 = null;
let engineObjectURL = null;

let chessBoardElem = null;
let turn = '-';
let playerColor = null;
let lastFen = null;

let uiChessBoard = null;

let activeGuiMoveHighlights = [];
let activeSiteMoveHighlights = [];

let engineLogNum = 1;
let userscriptLogNum = 1;

function eloToTitle(elo) {
    return elo >= 2800 ? "Grand Master"
    : elo >= 2600 ? "International Master"
    : elo >= 2400 ? "Fide Master"
    : elo >= 2200 ? "National Master"
    : elo >= 2000 ? "Expert"
    : elo >= 1800 ? "Tournament Player"
    : elo >= 1700 ? "Experienced"
    : elo >= 1600 ? "Experienced"
    : elo >= 1400 ? "Intermediate"
    : elo >= 1200 ? "Average"
    : elo >= 1000 ? "Casual"
    : "Beginner";
}

const engineEloArr = [
    { elo: 1200, data: 'go depth 1' },
    { elo: 1300, data: 'go depth 2' },
    { elo: 1450, data: 'go depth 3' },
    { elo: 1750, data: 'go depth 4' },
    { elo: 2000, data: 'go depth 5' },
    { elo: 2200, data: 'go depth 6' },
    { elo: 2400, data: 'go depth 7' },
    { elo: 2600, data: 'go depth 8' },
    { elo: 2800, data: 'go depth 9' },
    { elo: 3000, data: 'go depth 10' },
    { elo: 3200, data: 'go depth 11' },
    { elo: 3300, data: 'go depth 12' },
    { elo: 3400, data: 'go depth 13' }
];

function getCurrentEngineElo() {
    return engineEloArr.find(x => x.data == GM_getValue(dbValues.engineDepthQuery))?.elo;
}

function getEloDescription(elo,depth){


	return `Power: ${elo}, Desc: (${eloToTitle(elo)}), DEPTH: ${Number(depth)+1}`;
}



const Gui = new UserGui;
Gui.settings.window.title = 'A.C.A.S';
Gui.settings.window.external = true;
Gui.settings.window.size.width = 500;
Gui.settings.gui.external.popup = false;
Gui.settings.gui.external.style += GM_getResourceText('chessboard.css');
Gui.settings.gui.external.style += `
div[class^='board'] {
    background-color: black;
}
.best-move-from {
    background-color: #31ff7f;
    transform: scale(0.85);
}
.best-move-to {
    background-color: #31ff7f;
}
.negative-best-move-from {
    background-color: #fd0000;
    transform: scale(0.85);
}
.negative-best-move-to {
    background-color: #fd0000;
}
body {
    display: block;
    margin-left: auto;
    margin-right: auto;
    width: 360px;
}
#fen {
    margin-left: 10px;
}
#engine-log-container {
    max-height: 35vh;
    overflow: auto!important;
}
#userscript-log-container {
    max-height: 35vh;
    overflow: auto!important;
}
.sideways-card {
    display: flex;
    align-items: center;
    justify-content: space-between;
}
.rendered-form .card {
    margin-bottom: 10px;
}
.hidden {
    display: none;
}
.main-title-bar {
    display: flex;
    justify-content: space-between;
}
@keyframes wiggle {
    0% { transform: scale(1); }
   80% { transform: scale(1); }
   85% { transform: scale(1.1); }
   95% { transform: scale(1); }
  100% { transform: scale(1); }
}

.wiggle {
  display: inline-block;
  animation: wiggle 1s infinite;
}
`;

function FenUtils() {
    this.board = [
            [1,1,1,1,1,1,1,1],
            [1,1,1,1,1,1,1,1],
            [1,1,1,1,1,1,1,1],
            [1,1,1,1,1,1,1,1],
            [1,1,1,1,1,1,1,1],
            [1,1,1,1,1,1,1,1],
            [1,1,1,1,1,1,1,1],
            [1,1,1,1,1,1,1,1],
        ];

    this.pieceCodeToFen = pieceStr => {
        const [pieceColor, pieceName] = pieceStr.split('');

        return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
    }

    this.getFenCodeFromPieceElem = pieceElem => {
        return this.pieceCodeToFen([...pieceElem.classList].find(x => x.match(/^(b|w)[prnbqk]{1}$/)));
    }

    this.getPieceColor = pieceFenStr => {
        return pieceFenStr == pieceFenStr.toUpperCase() ? 'w' : 'b';
    }

    this.getPieceOppositeColor = pieceFenStr => {
        return this.getPieceColor(pieceFenStr) == 'w' ? 'b' : 'w';
    }

    this.squeezeEmptySquares = fenStr => {
        return fenStr.replace(/11111111/g, '8')
            .replace(/1111111/g, '7')
            .replace(/111111/g, '6')
            .replace(/11111/g, '5')
            .replace(/1111/g, '4')
            .replace(/111/g, '3')
            .replace(/11/g, '2');
    }

    this.posToIndex = pos => {
        const [x, y] = pos.split('');

        return { 'y': 8 - y, 'x': 'abcdefgh'.indexOf(x) };
    }

    this.getBoardPiece = pos => {
        const indexObj = this.posToIndex(pos);

        return this.board[indexObj.y][indexObj.x];
    }

    this.getRights = () => {
        let rights = '';

        // check for white
        const e1 = this.getBoardPiece('e1'),
              h1 = this.getBoardPiece('h1'),
              a1 = this.getBoardPiece('a1');

        if(e1 == 'K' && h1 == 'R') rights += 'K';
        if(e1 == 'K' && a1 == 'R') rights += 'Q';

        //check for black
        const e8 = this.getBoardPiece('e8'),
              h8 = this.getBoardPiece('h8'),
              a8 = this.getBoardPiece('a8');

        if(e8 == 'k' && h8 == 'r') rights += 'k';
        if(e8 == 'k' && a8 == 'r') rights += 'q';

        return rights ? rights : '-';
    }
	
	

    this.getBasicFen = () => {
        const pieceElems = [...chessBoardElem.querySelectorAll('.piece')];

        pieceElems.forEach(pieceElem => {
            const pieceFenCode = this.getFenCodeFromPieceElem(pieceElem);
            const [xPos, yPos] = pieceElem.classList.toString().match(/square-(\d)(\d)/).slice(1);

            this.board[8 - yPos][xPos - 1] = pieceFenCode;
        });

        const basicFen = this.squeezeEmptySquares(this.board.map(x => x.join('')).join('/'));

        return basicFen;
    }

    this.getFen = () => {
        const basicFen = this.getBasicFen();
        const rights = this.getRights();
		
	
        return `${basicFen} ${turn} ${rights} - 0 1`;
    }
}

function InterfaceUtils() {
    this.boardUtils = {
        findSquareElem: (squareCode) => {
            if(!Gui?.document) return;

            return Gui.document.querySelector(`.square-${squareCode}`);
        },
        markMove: (fromSquare, toSquare, isPlayerTurn) => {
            if(!Gui?.document) return;

            const [fromElem, toElem] = [this.boardUtils.findSquareElem(fromSquare), this.boardUtils.findSquareElem(toSquare)];

            if(isPlayerTurn) {
                fromElem.classList.add('best-move-from');
                toElem.classList.add('best-move-to');
            } else {
                fromElem.classList.add('negative-best-move-from');
                toElem.classList.add('negative-best-move-to');
            }

            activeGuiMoveHighlights.push(fromElem);
            activeGuiMoveHighlights.push(toElem);
        },
        removeBestMarkings: () => {
            if(!Gui?.document) return;

            activeGuiMoveHighlights.forEach(elem => {
                elem.classList.remove('best-move-from', 'best-move-to', 'negative-best-move-from', 'negative-best-move-to');
            });

            activeGuiMoveHighlights = [];
        },
        updateBoardFen: fen => {
            if(!Gui?.document) return;

            Gui.document.querySelector('#fen').textContent = fen;
        },
		updateBoardPower: (myScore,enemyScore) => {
            if(!Gui?.document) return;

            Gui.document.querySelector('#enemy-score').textContent = enemyScore;
            Gui.document.querySelector('#my-score').textContent = myScore;
        },
        updateBoardOrientation: orientation => {
            if(!Gui?.document) return;

            const orientationElem = Gui?.document?.querySelector('#orientation');

            if(orientationElem) {
                orientationElem.textContent = orientation;
            }
        }
    }

    this.engineLog = str => {
        if(!Gui?.document || enableLog==0) return;

        const logElem = document.createElement('div');
        logElem.classList.add('list-group-item');

        if(str.includes('info')) logElem.classList.add('list-group-item-info');
        if(str.includes('bestmove')) logElem.classList.add('list-group-item-success');

        logElem.innerText = `#${engineLogNum++} ${str}`;

        Gui.document.querySelector('#engine-log-container').prepend(logElem);
    }

    this.log = str => {
        if(!Gui?.document || enableLog==0) return;

        const logElem = document.createElement('div');
        logElem.classList.add('list-group-item');

        if(str.includes('info')) logElem.classList.add('list-group-item-info');
        if(str.includes('bestmove')) logElem.classList.add('list-group-item-success');

        const container = Gui?.document?.querySelector('#userscript-log-container');

        if(container) {
            logElem.innerText = `#${userscriptLogNum++} ${str}`;

            container.prepend(logElem);
        }
    }

    this.getBoardOrientation = () => {
        return document.querySelector('.board.flipped') ? 'b' : 'w';
    }

    this.updateBestMoveProgress = text => {
        if(!Gui?.document) return;

        const progressBarElem = Gui.document.querySelector('#best-move-progress');

        progressBarElem.innerText = text;

        progressBarElem.classList.remove('hidden');
        progressBarElem.classList.add('wiggle');
    }

    this.stopBestMoveProcessingAnimation = () => {
        if(!Gui?.document) return;

        const progressBarElem = Gui.document.querySelector('#best-move-progress');

        progressBarElem.classList.remove('wiggle');
    }

    this.hideBestMoveProgress = () => {
        if(!Gui?.document) return;

        const progressBarElem = Gui.document.querySelector('#best-move-progress');

        if(!progressBarElem.classList.contains('hidden')) {
            progressBarElem.classList.add('hidden');
            this.stopBestMoveProcessingAnimation();
        }
    }
}

function LozzaUtility() {
    this.separateMoveCodes = moveCode => {
        moveCode = moveCode.trim();

        let move = moveCode.split(' ')[1];

        return [move.slice(0,2), move.slice(2,4)];
    }

    this.extractInfo = str => {
        const keys = ['time', 'nps', 'depth'];

        return keys.reduce((acc, key) => {
            const match = str.match(`${key} (\\d+)`);

            if (match) {
                acc[key] = Number(match[1]);
            }

            return acc;
        }, {});
    }
}

function fenSquareToChessComSquare(fenSquareCode) {
    const [x, y] = fenSquareCode.split('');

    return `square-${['abcdefgh'.indexOf(x) + 1]}${y}`;
}

function markMoveToSite(fromSquare, toSquare, isPlayerTurn) {
    const highlight = (fenSquareCode, style) => {
        const squareClass = fenSquareToChessComSquare(fenSquareCode);

        const highlightElem = document.createElement('div');
            highlightElem.classList.add('highlight');
            highlightElem.classList.add(squareClass);
            highlightElem.dataset.testElement = 'highlight';
            highlightElem.style = style;

        activeSiteMoveHighlights.push(highlightElem);

        const existingHighLight = document.querySelector(`.highlight.${squareClass}`);

        if(existingHighLight) {
            existingHighLight.remove();
        }

        chessBoardElem.prepend(highlightElem);
    }

    const defaultFromSquareStyle = 'background-color: rgb(249 121 255); border: 4px solid rgb(0 0 0 / 50%);';
    const defaultToSquareStyle = 'background-color: rgb(129 129 129); border: 4px dashed rgb(0 0 0 / 50%);';

    const subtleMode = GM_getValue(dbValues.subtleMode);
    const subtletiness = GM_getValue(dbValues.subtletiness);

    highlight(fromSquare, subtleMode ? `background-color: rgb(0 0 0 / ${subtletiness}%);` : defaultFromSquareStyle);
    highlight(toSquare, subtleMode ? `background-color: rgb(0 0 0 / ${subtletiness}%);` : defaultToSquareStyle);
}

function removeSiteMoveMarkings() {
    activeSiteMoveHighlights.forEach(elem => {
        elem?.remove();
    });

    activeSiteMoveHighlights = [];
}

function updateBestMove(mutationArr) {
	if(SEND_MOVES_TO_CHESSAPP)
		sendMoves();
	
    const Fen = new FenUtils();
	
    let currentFen = Fen.getFen();
    let currentPgn = getPGN();

    if(currentFen != lastFen || currentPgn!=pgn) {
        lastFen = currentFen;
		pgn = currentPgn;

        if(mutationArr) {
            const attributeMutationArr = mutationArr.filter(m => m.target.classList.contains('piece') && m.attributeName == 'class');

            if(attributeMutationArr?.length) {
                turn = Fen.getPieceOppositeColor(Fen.getFenCodeFromPieceElem(attributeMutationArr[0].target));
                Interface.log(`Turn updated to ${turn}!`);
            }
        }
		
		if(engineIndex!=4)
			reloadChessEngine();

        Interface.stopBestMoveProcessingAnimation();

        currentFen = Fen.getFen();

        Interface.boardUtils.removeBestMarkings();

        removeSiteMoveMarkings();

        Interface.boardUtils.updateBoardFen(currentFen);


		Interface.log('Sending best move request to the engine!');
		if(engineIndex!=4){
			engine.postMessage(`position fen ${currentFen}`);
		
			if(playerColor == null || turn == playerColor) {
				engine.postMessage(GM_getValue(dbValues.engineDepthQuery));
			}
		}else{
			// lichess engine
			if(playerColor == null || turn == playerColor) {
				let depth = engineEloArr.findIndex(x => x.data == GM_getValue(dbValues.engineDepthQuery))+1;
				getBestMoves2(currentFen,depth);
			}
			
	
		}


    }
}

function observeNewMoves() {
    updateBestMove();

    const boardObserver = new MutationObserver(mutationArr => {
        const lastPlayerColor = playerColor;

        updatePlayerColor();
		

        if(playerColor != lastPlayerColor) {
            Interface.log(`Player color changed from ${lastPlayerColor} to ${playerColor}!`);
			
            updateBestMove();
        } else {
            updateBestMove(mutationArr);
        }
    });

    boardObserver.observe(chessBoardElem, { childList: true, subtree: true, attributes: true });
}

function addGuiPages() {
    if(Gui?.document) return;

    Gui.addPage("Main", `
    <div class="rendered-form">
        <script>${GM_getResourceText('jquery.js')}</script>
        <script>${GM_getResourceText('chessboard.js')}</script>
        <div class="card">
            <div class="card-body">
                <div class="main-title-bar">
                    <h5 class="card-title">Live Chessboard</h5>
                    <p id="best-move-progress"></p>
                </div>

                <div id="board" style="width: 447px"></div>
            </div>
            <div id="orientation" class="hidden"></div>
            <div class="card-footer sideways-card">FEN :<small class="text-muted"><div id="fen"></div></small></div>
            <div class="card-footer sideways-card">ENEMY SCORE :<div id="enemy-score"></div></div>
            <div class="card-footer sideways-card">MY SCORE : <div id="my-score"></div></div>
        </div>
        <script>
        const orientationElem = document.querySelector('#orientation');
        const fenElem = document.querySelector('#fen');

        let board = ChessBoard('board', {
            pieceTheme: '${repositoryRawURL}/content/chesspieces/{piece}.svg',
            position: 'start',
            orientation: '${playerColor == 'b' ? 'black' : 'white'}'
        });

        const orientationObserver = new MutationObserver(() => {
            board = ChessBoard('board', {
                pieceTheme: '${repositoryRawURL}/content/chesspieces/{piece}.svg',
                position: fenElem.textContent,
                orientation: orientationElem.textContent == 'b' ? 'black' : 'white'
            });
        });

        const fenObserver = new MutationObserver(() => {
            board.position(fenElem.textContent);
        });

        orientationObserver.observe(orientationElem, { attributes: true,  childList: true,  characterData: true });
        fenObserver.observe(fenElem, { attributes: true,  childList: true,  characterData: true });
        </script>
    </div>
    `);

    Gui.addPage('Log', `
    <div class="rendered-form">
        <div class="card">
            <div class="card-body">
                <h5 class="card-title">Userscript Log</h5>
                <ul class="list-group" id="userscript-log-container"></ul>
            </div>
        </div>
        <div class="card">
            <div class="card-body">
                <h5 class="card-title">Engine Log</h5>
                <ul class="list-group" id="engine-log-container"></ul>
            </div>
        </div>
    </div>
    `);

    const depth = engineEloArr.findIndex(x => x.data == GM_getValue(dbValues.engineDepthQuery));
    const subtletiness = GM_getValue(dbValues.subtletiness);

    const displayMovesOnSite = GM_getValue(dbValues.displayMovesOnSite) == true;
    const openGuiAutomatically = GM_getValue(dbValues.openGuiAutomatically) == true;
    const subtleMode = GM_getValue(dbValues.subtleMode) == true;




    Gui.addPage('Settings', `
    <div class="rendered-form">
        <div class="card">
            <div class="card-body">
                <h5 class="card-title">Engine</h5>
                <div class="form-group field-select-engine">
                    <select class="form-control" name="select-engine" id="select-engine">
                        <option value="option-lozza" id="select-engine-0">Lozza</option>
                        <option value="option-lozza2" id="select-engine-1">Lozza 2</option>
                        <option value="option-stockfish" id="select-engine-2">Stockfish</option>
                        <option value="option-stockfish2" id="select-engine-3">Stockfish 2</option>
                        <option value="option-nodeserver" id="select-engine-4">Node Server</option>
                    </select>
                </div>
            </div>
        </div>
		
        <div class="card">
            <div class="card-body">
                <h5 class="card-title">Engine Strength</h5>
                <input type="range" class="form-range" min="0" max="${engineEloArr.length - 1}" value="${depth}" id="depth-range">
            </div>
            <div class="card-footer sideways-card" id="depth-elo">Elo <small id="elo">${getEloDescription(getCurrentEngineElo(),depth)}</small></div>
        </div>
		
        <div class="card">
            <div class="card-body">
                <h5 class="card-title">Visual</h5>
                <div id="display-moves-on-site-warning" class="alert alert-danger ${displayMovesOnSite ? '' : 'hidden'}">
                    <strong>Highly risky!</strong> DOM manipulation (moves displayed on site) is easily detectable! Use with caution.
                </div>
                <input type="checkbox" id="display-moves-on-site" ${displayMovesOnSite ? 'checked' : ''}>
                <label for="display-moves-on-site">Display moves on site</label>
                <!-- Display moves on site additional settings -->
                <div class="card ${displayMovesOnSite ? '' : 'hidden'}" id="display-moves-on-site-additional">
                    <div class="card-body">
                        <!-- Open GUI automatically checkbox -->
                        <div>
                            <input type="checkbox" id="open-gui-automatically" ${openGuiAutomatically ? 'checked' : ''}>
                            <label for="open-gui-automatically">Open GUI automatically</label>
                        </div>
                        <!-- Subtle mode settinngs -->
                        <div>
                            <!-- Subtle mode checkbox -->
                            <div>
                                <input type="checkbox" id="subtle-mode" ${subtleMode ? 'checked' : ''}>
                                <label for="subtle-mode">Subtle mode</label>
                            </div>
                            <!-- Subtle mode additional settings -->
                            <div>
                                <div class="card ${subtleMode ? '' : 'hidden'}" id="subtletiness-range-container">
                                    <div class="card-body">
                                        <!-- Subtletiness range -->
                                        <h6 class="card-title">Visibility</h6>
                                        <input type="range" class="form-range" min="1" max="50" value="${subtletiness}" id="subtletiness-range">
                                    </div>
                                    <div class="card-footer sideways-card">Percentage <small id="subtletiness-info">${subtletiness}%</small></div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    `);

    Gui.addPage('About', `
    <div class="rendered-form">
        <div class="card">
            <div class="card-body">
                <p class="lead">
                  <b>A.C.A.S</b> <i>(Advanced Chess Assistance System)</i> is an advanced chess assistance system which enhances your chess performance with cutting-edge real-time move analysis.
                </p>
            </div>
            <div class="card-footer sideways-card">Developers <small>Haka</small></div>
            <div class="card-footer sideways-card">Version <small>${GM_info.script.version}</small></div>
            <div class="card-footer sideways-card">Repository <a href="${repositoryURL}" target="_blank">A.C.A.S</a></div>
        </div>
    </div>
    `);
}

function openGUI() {
    Interface.log(`Opening GUI!`);

    const hide = elem => elem.classList.add('hidden');
    const show = elem => elem.classList.remove('hidden');

    Gui.open(() => {
		const depthEloElem = Gui.document.querySelector('#depth-elo');
        const depthRangeElem = Gui.document.querySelector('#depth-range');
        const eloElem = Gui.document.querySelector('#elo');
		const engineElem = Gui.document.querySelector('#select-engine');
		engineElem.selectedIndex=engineIndex;


        const displayMovesOnSiteElem = Gui.document.querySelector('#display-moves-on-site');
        const displayMovesOnSiteWarningElem = Gui.document.querySelector('#display-moves-on-site-warning');

        const openGuiAutomaticallyElem = Gui.document.querySelector('#open-gui-automatically');
        const openGuiAutomaticallyAdditionalElem = Gui.document.querySelector('#display-moves-on-site-additional');

        const subtleModeElem = Gui.document.querySelector('#subtle-mode');
        const subtletinessRangeContainerElem = Gui.document.querySelector('#subtletiness-range-container');
        const subtletinessRange = Gui.document.querySelector('#subtletiness-range');

        const subtletinessInfo = Gui.document.querySelector('#subtletiness-info');

		engineElem.onchange = () =>{
			engineIndex=engineElem.selectedIndex;
			
			

			if(engineObjectURL){
				URL.revokeObjectURL(engineObjectURL);
				engineObjectURL=null;
			}

			

			Interface.stopBestMoveProcessingAnimation();

			Interface.boardUtils.removeBestMarkings();

			removeSiteMoveMarkings();
			
			
			if(engineIndex==4){
				Interface.boardUtils.updateBoardPower(0,0);
			}else{

				if(engine!=null)
					engine.terminate();
				
				loadChessEngine();
			}
		}

        depthRangeElem.onchange = () => {
            const depth = depthRangeElem.value;
            const engineEloObj = engineEloArr[depth];

            const description = getEloDescription(engineEloObj.elo,depth);
            const engineQuery = engineEloObj.data;

            GM_setValue(dbValues.engineDepthQuery, engineQuery);

            eloElem.innerText = description;
        };

        displayMovesOnSiteElem.onchange = () => {
            const isChecked = displayMovesOnSiteElem.checked;

            if(isChecked) {
                GM_setValue(dbValues.displayMovesOnSite, true);

                show(displayMovesOnSiteWarningElem);
                show(openGuiAutomaticallyAdditionalElem);

                openGuiAutomaticallyElem.checked = GM_getValue(dbValues.openGuiAutomatically);
            } else {
                GM_setValue(dbValues.displayMovesOnSite, false);
                GM_setValue(dbValues.openGuiAutomatically, true);

                hide(displayMovesOnSiteWarningElem);
                hide(openGuiAutomaticallyAdditionalElem);
            }
        };

        openGuiAutomaticallyElem.onchange = () => {
            GM_setValue(dbValues.openGuiAutomatically, openGuiAutomaticallyElem.checked);
        };

        subtleModeElem.onchange = () => {
           const isChecked = subtleModeElem.checked;

            if(isChecked) {
                GM_setValue(dbValues.subtleMode, true);
                show(subtletinessRangeContainerElem);
            } else {
                GM_setValue(dbValues.subtleMode, false);
                hide(subtletinessRangeContainerElem);
            }
        };

        subtletinessRange.onchange = () => {
            GM_setValue(dbValues.subtletiness, subtletinessRange.value);
            subtletinessInfo.innerText = `${subtletinessRange.value}%`;
        };

        window.onunload = () => {
            if(Gui.window && !Gui.window.closed) {
                Gui.window.close();
            }
        };

        const isWindowClosed = setInterval(() => {
            if(Gui.window.closed) {
                clearInterval(isWindowClosed);
				if(engine!=null)
					engine.terminate();
            }
        }, 1000);

        observeNewMoves();

        Interface.log('Initialized!');
    });
}

function reloadChessEngine() {
	if(reload_count>=reload_every){
		reload_count=0;
		Interface.log(`Reloading the chess engine!`);

		engine.terminate();
		loadChessEngine();
	}
	else{
		reload_count=reload_count+1;

	}
}

function loadRandomChessengine(fen){
	if(!engineObjectURL2)
		engineObjectURL2 = URL.createObjectURL(new Blob([GM_getResourceText('stockfish2.js')], {type: 'application/javascript'}));
	

	
	if(!engine2)
		engine2 = new Worker(engineObjectURL2);
	
	
	if(engineObjectURL2){
	engine2.onmessage = e => {
            if(e.data.includes('bestmove')) {


                const [from, to] = LozzaUtils.separateMoveCodes(e.data);
				
				if(!lichess_worked)
					moveResult(from,to,0);
            }

         
        };
		
		engine2.postMessage('ucinewgame');
		
		engine2.postMessage(`position fen ${fen}`);
		
	
		engine2.postMessage(GM_getValue(dbValues.engineDepthQuery));
		
	}
}

function loadChessEngine() {
    if(!engineObjectURL) {
		if(engineIndex==0)
			engineObjectURL = URL.createObjectURL(new Blob([GM_getResourceText('lozza.js')], {type: 'application/javascript'}));
		else if(engineIndex==1)
			engineObjectURL = URL.createObjectURL(new Blob([GM_getResourceText('lozza2.js')], {type: 'application/javascript'}));
		else if(engineIndex==2)
			engineObjectURL = URL.createObjectURL(new Blob([GM_getResourceText('stockfish.js')], {type: 'application/javascript'}));
		else if(engineIndex==3)
			engineObjectURL = URL.createObjectURL(new Blob([GM_getResourceText('stockfish2.js')], {type: 'application/javascript'}));
    }

    if(engineObjectURL) {
		engine = new Worker(engineObjectURL);

        engine.onmessage = e => {
            if(e.data.includes('bestmove')) {
                removeSiteMoveMarkings();
				Interface.boardUtils.removeBestMarkings();

                const [from, to] = LozzaUtils.separateMoveCodes(e.data);
                
				moveResult(from,to,0);
            }

            else if(e.data.includes('info')) {
                removeSiteMoveMarkings();
				Interface.boardUtils.removeBestMarkings();

                const infoObj = LozzaUtils.extractInfo(e.data);

                if(infoObj?.depth) {
                    Interface.updateBestMoveProgress(`Depth ${infoObj.depth}`);
                }
            }
			Interface.engineLog(e.data);
        };

        engine.postMessage('ucinewgame');

        Interface.log(`Loaded the chess engine!`);
    }
}

function initializeDatabase() {
    const initValue = (name, value) => {
        if(GM_getValue(name) == undefined) {
            GM_setValue(name, value);
        }
    };

    initValue(dbValues.engineDepthQuery, 'go depth 5');
    initValue(dbValues.displayMovesOnSite, false);
    initValue(dbValues.subtleMode, false);
    initValue(dbValues.openGuiAutomatically, true);
    initValue(dbValues.subtletiness, 25);

    Interface.log(`Initialized the database!`);
}

async function updatePlayerColor() {
    const boardOrientation = Interface.getBoardOrientation();

    playerColor = boardOrientation;
    turn = boardOrientation;

    Interface.boardUtils.updateBoardOrientation(playerColor);
}

async function initialize(openInterface) {
    Interface = new InterfaceUtils();
    LozzaUtils = new LozzaUtility();

    const boardOrientation = Interface.getBoardOrientation();
    turn = boardOrientation;

    initializeDatabase();

    loadChessEngine();

    updatePlayerColor();

    if(openInterface) {
        addGuiPages();
        openGUI();
    } else {
        observeNewMoves();
    }
}

if(typeof GM_registerMenuCommand == 'function') {
    GM_registerMenuCommand("Open A.C.A.S", e => {
        if(chessBoardElem) {
            initialize(true);
        }
    }, 's');
}

const waitForChessBoard = setInterval(() => {
    const boardElem = document.querySelector('chess-board');
    const firstPieceElem = document.querySelector('.piece');

    if(boardElem && firstPieceElem && chessBoardElem != boardElem) {
        chessBoardElem = boardElem;

        if(window.location.href != 'https://www.chess.com/play') {
            const openGuiAutomatically = GM_getValue(dbValues.openGuiAutomatically);

            if(openGuiAutomatically == undefined) {
                initialize(true);
            } else {
                initialize(openGuiAutomatically);
            }
        }
    }
}, 1000);