// ==UserScript==
// @name Chesshook greasyfork
// @include https://www.chess.com/*
// @grant none
// @version 2.0
// @author 0mlml
// @description Chess.com Cheat Userscript
// @run-at document-start
// @license MIT
// @namespace https://greasyfork.org/users/1283801
// ==/UserScript==
// https://github.com/Strryke/betafish/blob/f281791317d766cb5deaf453a221da632b67df1c/js/betafish.js
// Modified 14.2.2023 to expose the best move function.
// Modified 23.2.2023 to change the generic name and comment out searchcontroller logging
// Modified 27.2.2023 to diable additional logging
const betafishEngine = function() {
/****************************\
============================
Board Constants
============================
\****************************/
const START_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
const PceChar = ".PNBRQKpnbrqk";
const SideChar = "wb-";
const RankChar = "12345678";
const FileChar = "abcdefgh";
const MFLAGEP = 0x40000;
const MFLAGPS = 0x80000;
const MFLAGCA = 0x1000000;
const MFLAGCAP = 0x7c000;
const MFLAGPROM = 0xf00000;
const NOMOVE = 0;
// prettier-ignore
const CastlePerm = [
15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 13, 15, 15, 15, 12, 15, 15, 14, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 7, 15, 15, 15, 3, 15, 15, 11, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15
];
//prettier-ignore
{
var PIECES = { EMPTY: 0, wP: 1, wN: 2, wB: 3, wR: 4, wQ: 5, wK: 6, bP: 7, bN: 8, bB: 9, bR: 10, bQ: 11, bK: 12 };
var BRD_SQ_NUM = 120;
var FILES = {
FILE_A: 0, FILE_B: 1, FILE_C: 2, FILE_D: 3,
FILE_E: 4, FILE_F: 5, FILE_G: 6, FILE_H: 7, FILE_NONE: 8
};
var RANKS = {
RANK_1: 0, RANK_2: 1, RANK_3: 2, RANK_4: 3,
RANK_5: 4, RANK_6: 5, RANK_7: 6, RANK_8: 7, RANK_NONE: 8
};
var COLOURS = { WHITE: 0, BLACK: 1, BOTH: 2 };
var CASTLEBIT = { whiteKing: 1, whiteQueen: 2, blackKing: 4, blackQueen: 8 };
var SQUARES = {
A1: 21, B1: 22, C1: 23, D1: 24, E1: 25, F1: 26, G1: 27, H1: 28,
A8: 91, B8: 92, C8: 93, D8: 94, E8: 95, F8: 96, G8: 97, H8: 98,
NO_SQ: 99, OFFBOARD: 100
};
var MAXGAMEMOVES = 2048;
var MAXPOSITIONMOVES = 256;
var MAXDEPTH = 64;
var INFINITE = 30000;
var MATE = 29000;
var PVENTRIES = 10000;
var FilesBrd = new Array(BRD_SQ_NUM);
var RanksBrd = new Array(BRD_SQ_NUM);
}
// prettier-ignore
{
var PieceVal = [0, 100, 325, 325, 550, 1000, 50000, 100, 325, 325, 550, 1000, 50000];
var PieceCol = [COLOURS.BOTH, COLOURS.WHITE, COLOURS.WHITE, COLOURS.WHITE, COLOURS.WHITE, COLOURS.WHITE, COLOURS.WHITE,
COLOURS.BLACK, COLOURS.BLACK, COLOURS.BLACK, COLOURS.BLACK, COLOURS.BLACK, COLOURS.BLACK];
var PiecePawn = [0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0];
var PieceKnight = [0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0];
var PieceKing = [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1];
var PieceRookQueen = [0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0];
var PieceBishopQueen = [0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0];
var PieceSlides = [0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0];
var Kings = [PIECES.wK, PIECES.bK];
var KnDir = [-8, -19, -21, -12, 8, 19, 21, 12];
var RkDir = [-1, -10, 1, 10];
var BiDir = [-9, -11, 11, 9];
var KiDir = [-1, -10, 1, 10, -9, -11, 11, 9];
var DirNum = [0, 0, 8, 4, 4, 8, 8, 0, 8, 4, 4, 8, 8];
var PceDir = [0, 0, KnDir, BiDir, RkDir, KiDir, KiDir, 0, KnDir, BiDir, RkDir, KiDir, KiDir];
var LoopNonSlidePce = [PIECES.wN, PIECES.wK, 0, PIECES.bN, PIECES.bK, 0];
var LoopNonSlideIndex = [0, 3];
var LoopSlidePce = [PIECES.wB, PIECES.wR, PIECES.wQ, 0, PIECES.bB, PIECES.bR, PIECES.bQ, 0];
var LoopSlideIndex = [0, 4];
var PieceKeys = new Array(14 * 120);
var SideKey;
var CastleKeys = new Array(16);
var Sq120ToSq64 = new Array(BRD_SQ_NUM);
var Sq64ToSq120 = new Array(64);
var Mirror64 = [
56, 57, 58, 59, 60, 61, 62, 63, 48, 49, 50, 51, 52, 53, 54, 55, 40, 41, 42,
43, 44, 45, 46, 47, 32, 33, 34, 35, 36, 37, 38, 39, 24, 25, 26, 27, 28, 29,
30, 31, 16, 17, 18, 19, 20, 21, 22, 23, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2,
3, 4, 5, 6, 7,
];
}
// init
function InitFilesRanksBrd() {
var index = 0;
var file = FILES.FILE_A;
var rank = RANKS.RANK_1;
var sq = SQUARES.A1;
for (index = 0; index < BRD_SQ_NUM; ++index) {
FilesBrd[index] = SQUARES.OFFBOARD;
RanksBrd[index] = SQUARES.OFFBOARD;
}
for (rank = RANKS.RANK_1; rank <= RANKS.RANK_8; ++rank) {
for (file = FILES.FILE_A; file <= FILES.FILE_H; ++file) {
sq = fileRanktoSquare(file, rank);
FilesBrd[sq] = file;
RanksBrd[sq] = rank;
}
}
}
function InitHashKeys() {
for (index = 0; index < 14 * 120; ++index) {
PieceKeys[index] = getRand32();
}
SideKey = getRand32();
for (index = 0; index < 16; ++index) {
CastleKeys[index] = getRand32();
}
}
function InitSq120To64() {
var file = FILES.FILE_A;
var rank = RANKS.RANK_1;
var sq = SQUARES.A1;
var sq64 = 0;
for (index = 0; index < BRD_SQ_NUM; ++index) {
Sq120ToSq64[index] = 65;
}
for (index = 0; index < 64; ++index) {
Sq64ToSq120[index] = 120;
}
for (rank = RANKS.RANK_1; rank <= RANKS.RANK_8; ++rank) {
for (file = FILES.FILE_A; file <= FILES.FILE_H; ++file) {
sq = fileRanktoSquare(file, rank);
Sq64ToSq120[sq64] = sq;
Sq120ToSq64[sq] = sq64;
sq64++;
}
}
}
function InitBoardVars() {
var index = 0;
for (index = 0; index < MAXGAMEMOVES; ++index) {
GameBoard.history.push({
move: NOMOVE,
castlePerm: 0,
enPas: 0,
fiftyMove: 0,
posKey: 0,
});
}
for (index = 0; index < PVENTRIES; ++index) {
GameBoard.PvTable.push({
move: NOMOVE,
posKey: 0,
});
}
}
function init() {
InitFilesRanksBrd();
InitHashKeys();
InitSq120To64();
InitBoardVars();
InitMvvLva();
ParseFen(START_FEN);
}
/****************************\
============================
Board Helper Functions
============================
\****************************/
function getRand32() {
return (
(Math.floor(Math.random() * 255 + 1) << 23) |
(Math.floor(Math.random() * 255 + 1) << 16) |
(Math.floor(Math.random() * 255 + 1) << 8) |
Math.floor(Math.random() * 255 + 1)
);
}
function fileRanktoSquare(f, r) {
return 21 + f + r * 10;
}
function isSqOffBoard(sq) {
return FilesBrd[sq] == SQUARES.OFFBOARD;
}
function hashPiece(pce, sq) {
GameBoard.posKey ^= PieceKeys[pce * 120 + sq];
}
function hashCastle() {
GameBoard.posKey ^= CastleKeys[GameBoard.castlePerm];
}
function hashSide() {
GameBoard.posKey ^= SideKey;
}
function hashEnPas() {
GameBoard.posKey ^= PieceKeys[GameBoard.enPas];
}
function sq120to64(sq120) {
return Sq120ToSq64[sq120];
}
function sq64to120(sq64) {
return Sq64ToSq120[sq64];
}
function getPieceIndex(pce, pceNum) {
return pce * 10 + pceNum;
}
function mirror64(sq) {
return Mirror64[sq];
}
/*
0000 0000 0000 0000 0000 0111 1111 -> From 0x7F
0000 0000 0000 0011 1111 1000 0000 -> To >> 7, 0x7F
0000 0000 0011 1100 0000 0000 0000 -> Captured >> 14, 0xF
0000 0000 0100 0000 0000 0000 0000 -> EP 0x40000
0000 0000 1000 0000 0000 0000 0000 -> Pawn Start 0x80000
0000 1111 0000 0000 0000 0000 0000 -> Promoted Piece >> 20, 0xF
0001 0000 0000 0000 0000 0000 0000 -> Castle 0x1000000
*/
function fromSQ(m) {
return m & 0x7f;
}
function toSQ(m) {
return (m >> 7) & 0x7f;
}
function CAPTURED(m) {
return (m >> 14) & 0xf;
}
function PROMOTED(m) {
return (m >> 20) & 0xf;
}
/****************************\
============================
Gameboard
============================
\****************************/
function getPieceIndex(pce, pceNum) {
return pce * 10 + pceNum;
}
let GameBoard = {};
GameBoard.pieces = new Array(BRD_SQ_NUM);
GameBoard.side = COLOURS.WHITE;
GameBoard.fiftyMove = 0;
GameBoard.hisPly = 0;
GameBoard.history = [];
GameBoard.ply = 0;
GameBoard.enPas = 0;
GameBoard.castlePerm = 0;
GameBoard.material = new Array(2); // WHITE,BLACK material of pieces
GameBoard.pceNum = new Array(13); // indexed by Pce
GameBoard.pList = new Array(14 * 10);
GameBoard.posKey = 0;
GameBoard.moveList = new Array(MAXDEPTH * MAXPOSITIONMOVES);
GameBoard.moveScores = new Array(MAXDEPTH * MAXPOSITIONMOVES);
GameBoard.moveListStart = new Array(MAXDEPTH);
GameBoard.PvTable = [];
GameBoard.PvArray = new Array(MAXDEPTH);
GameBoard.searchHistory = new Array(14 * BRD_SQ_NUM);
GameBoard.searchKillers = new Array(3 * MAXDEPTH);
GameBoard.GameOver = false;
function PrintBoard() {
var sq, file, rank, piece;
console.log("\nGame Board:\n");
for (rank = RANKS.RANK_8; rank >= RANKS.RANK_1; rank--) {
var line = RankChar[rank] + " ";
for (file = FILES.FILE_A; file <= FILES.FILE_H; file++) {
sq = fileRanktoSquare(file, rank);
piece = GameBoard.pieces[sq];
line += " " + PceChar[piece] + " ";
}
console.log(line);
}
console.log("");
var line = " ";
for (file = FILES.FILE_A; file <= FILES.FILE_H; file++) {
line += " " + FileChar[file] + " ";
}
console.log(line);
console.log("side:" + SideChar[GameBoard.side]);
console.log("enPas:" + GameBoard.enPas);
line = "";
if (GameBoard.castlePerm & CASTLEBIT.whiteKing) line += "K";
if (GameBoard.castlePerm & CASTLEBIT.whiteQueen) line += "Q";
if (GameBoard.castlePerm & CASTLEBIT.blackKing) line += "k";
if (GameBoard.castlePerm & CASTLEBIT.blackQueen) line += "q";
console.log("castle:" + line);
console.log("key:" + GameBoard.posKey.toString(16));
}
function GenerateFEN() {
var fenStr = "";
var rank, file, sq, piece;
var emptyCount = 0;
for (rank = RANKS.RANK_8; rank >= RANKS.RANK_1; rank--) {
emptyCount = 0;
for (file = FILES.FILE_A; file <= FILES.FILE_H; file++) {
sq = fileRanktoSquare(file, rank);
piece = GameBoard.pieces[sq];
if (piece == PIECES.EMPTY) {
emptyCount++;
} else {
if (emptyCount != 0) {
fenStr += emptyCount.toString();
}
emptyCount = 0;
fenStr += PceChar[piece];
}
}
if (emptyCount != 0) {
fenStr += emptyCount.toString();
}
if (rank != RANKS.RANK_1) {
fenStr += "/";
} else {
fenStr += " ";
}
}
fenStr += SideChar[GameBoard.side] + " ";
if (GameBoard.castlePerm == 0) {
fenStr += "- ";
} else {
if (GameBoard.castlePerm & CASTLEBIT.whiteKing) fenStr += "K";
if (GameBoard.castlePerm & CASTLEBIT.whiteQueen) fenStr += "Q";
if (GameBoard.castlePerm & CASTLEBIT.blackKing) fenStr += "k";
if (GameBoard.castlePerm & CASTLEBIT.blackQueen) fenStr += "q";
}
if (GameBoard.enPas == SQUARES.NO_SQ) {
fenStr += " -";
}
fenStr += " ";
fenStr += GameBoard.fiftyMove;
fenStr += " ";
var tempHalfMove = GameBoard.hisPly;
if (GameBoard.side == COLOURS.BLACK) {
tempHalfMove--;
}
fenStr += tempHalfMove / 2;
return fenStr;
}
function ParseMove(from, to) {
GenerateMoves();
var Move = NOMOVE;
var PromPce = PIECES.EMPTY;
var found = false;
for (
index = GameBoard.moveListStart[GameBoard.ply];
index < GameBoard.moveListStart[GameBoard.ply + 1];
++index
) {
Move = GameBoard.moveList[index];
if (fromSQ(Move) == from && toSQ(Move) == to) {
PromPce = PROMOTED(Move);
if (PromPce != PIECES.EMPTY) {
if (
(PromPce == PIECES.wQ && GameBoard.side == COLOURS.WHITE) ||
(PromPce == PIECES.bQ && GameBoard.side == COLOURS.BLACK)
) {
found = true;
break;
}
continue;
}
found = true;
break;
}
}
if (found != false) {
if (MakeMove(Move) == false) {
return NOMOVE;
}
TakeMove();
return Move;
}
return NOMOVE;
}
function GeneratePosKey() {
var sq = 0;
var finalKey = 0;
var piece = PIECES.EMPTY;
for (sq = 0; sq < BRD_SQ_NUM; ++sq) {
piece = GameBoard.pieces[sq];
if (piece != PIECES.EMPTY && piece != SQUARES.OFFBOARD) {
finalKey ^= PieceKeys[piece * 120 + sq];
}
}
if (GameBoard.side == COLOURS.WHITE) {
finalKey ^= SideKey;
}
if (GameBoard.enPas != SQUARES.NO_SQ) {
finalKey ^= PieceKeys[GameBoard.enPas];
}
finalKey ^= CastleKeys[GameBoard.castlePerm];
return finalKey;
}
function OppositePrSq(move) {
// takes in a move and returns the reverse of prsq
// b1 > 22
// c3 > 43
// e2 > 35
// e3 > 45
const file = {
a: 1,
b: 2,
c: 3,
d: 4,
e: 5,
f: 6,
g: 7,
h: 8,
};
let to120 = file[move[0]] + 10 * (parseInt(move[1]) + 1);
return to120;
}
function UpdateListsMaterial() {
var piece, sq, index, colour;
for (index = 0; index < 14 * 120; ++index) {
GameBoard.pList[index] = PIECES.EMPTY;
}
for (index = 0; index < 2; ++index) {
GameBoard.material[index] = 0;
}
for (index = 0; index < 13; ++index) {
GameBoard.pceNum[index] = 0;
}
for (index = 0; index < 64; ++index) {
sq = sq64to120(index);
piece = GameBoard.pieces[sq];
if (piece != PIECES.EMPTY) {
colour = PieceCol[piece];
GameBoard.material[colour] += PieceVal[piece];
GameBoard.pList[getPieceIndex(piece, GameBoard.pceNum[piece])] = sq;
GameBoard.pceNum[piece]++;
}
}
}
function ResetBoard() {
var index = 0;
for (index = 0; index < BRD_SQ_NUM; ++index) {
GameBoard.pieces[index] = SQUARES.OFFBOARD;
}
for (index = 0; index < 64; ++index) {
GameBoard.pieces[sq64to120(index)] = PIECES.EMPTY;
}
GameBoard.side = COLOURS.BOTH;
GameBoard.enPas = SQUARES.NO_SQ;
GameBoard.fiftyMove = 0;
GameBoard.ply = 0;
GameBoard.hisPly = 0;
GameBoard.castlePerm = 0;
GameBoard.posKey = 0;
GameBoard.moveListStart[GameBoard.ply] = 0;
GameBoard.GameOver = false;
}
//rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
function ParseFen(fen) {
ResetBoard();
var rank = RANKS.RANK_8;
var file = FILES.FILE_A;
var piece = 0;
var count = 0;
var i = 0;
var sq120 = 0;
var fenCnt = 0; // fen[fenCnt]
// prettier-ignore
while ((rank >= RANKS.RANK_1) && fenCnt < fen.length) {
count = 1;
switch (fen[fenCnt]) {
case 'p': piece = PIECES.bP; break;
case 'r': piece = PIECES.bR; break;
case 'n': piece = PIECES.bN; break;
case 'b': piece = PIECES.bB; break;
case 'k': piece = PIECES.bK; break;
case 'q': piece = PIECES.bQ; break;
case 'P': piece = PIECES.wP; break;
case 'R': piece = PIECES.wR; break;
case 'N': piece = PIECES.wN; break;
case 'B': piece = PIECES.wB; break;
case 'K': piece = PIECES.wK; break;
case 'Q': piece = PIECES.wQ; break;
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
piece = PIECES.EMPTY;
count = fen[fenCnt].charCodeAt() - '0'.charCodeAt();
break;
case '/':
case ' ':
rank--;
file = FILES.FILE_A;
fenCnt++;
continue;
default:
console.log("FEN error");
return;
}
for (i = 0; i < count; i++) {
sq120 = fileRanktoSquare(file, rank);
GameBoard.pieces[sq120] = piece;
file++;
}
fenCnt++;
} // while loop end
//rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
GameBoard.side = fen[fenCnt] == "w" ? COLOURS.WHITE : COLOURS.BLACK;
fenCnt += 2;
for (i = 0; i < 4; i++) {
if (fen[fenCnt] == " ") {
break;
}
switch (fen[fenCnt]) {
case "K":
GameBoard.castlePerm |= CASTLEBIT.whiteKing;
break;
case "Q":
GameBoard.castlePerm |= CASTLEBIT.whiteQueen;
break;
case "k":
GameBoard.castlePerm |= CASTLEBIT.blackKing;
break;
case "q":
GameBoard.castlePerm |= CASTLEBIT.blackQueen;
break;
default:
break;
}
fenCnt++;
}
fenCnt++;
if (fen[fenCnt] != "-") {
file = fen[fenCnt].charCodeAt() - "a".charCodeAt();
rank = fen[fenCnt + 1].charCodeAt() - "1".charCodeAt();
// console.log(
// "fen[fenCnt]:" + fen[fenCnt] + " File:" + file + " Rank:" + rank
// );
GameBoard.enPas = fileRanktoSquare(file, rank);
}
GameBoard.posKey = GeneratePosKey();
UpdateListsMaterial();
}
function SqAttacked(sq, side) {
var pce;
var t_sq;
var index;
if (side == COLOURS.WHITE) {
if (
GameBoard.pieces[sq - 11] == PIECES.wP ||
GameBoard.pieces[sq - 9] == PIECES.wP
) {
return true;
}
} else {
if (
GameBoard.pieces[sq + 11] == PIECES.bP ||
GameBoard.pieces[sq + 9] == PIECES.bP
) {
return true;
}
}
for (index = 0; index < 8; index++) {
pce = GameBoard.pieces[sq + KnDir[index]];
if (
pce != SQUARES.OFFBOARD &&
PieceCol[pce] == side &&
PieceKnight[pce] == true
) {
return true;
}
}
for (index = 0; index < 4; ++index) {
dir = RkDir[index];
t_sq = sq + dir;
pce = GameBoard.pieces[t_sq];
while (pce != SQUARES.OFFBOARD) {
if (pce != PIECES.EMPTY) {
if (PieceRookQueen[pce] == true && PieceCol[pce] == side) {
return true;
}
break;
}
t_sq += dir;
pce = GameBoard.pieces[t_sq];
}
}
for (index = 0; index < 4; ++index) {
dir = BiDir[index];
t_sq = sq + dir;
pce = GameBoard.pieces[t_sq];
while (pce != SQUARES.OFFBOARD) {
if (pce != PIECES.EMPTY) {
if (PieceBishopQueen[pce] == true && PieceCol[pce] == side) {
return true;
}
break;
}
t_sq += dir;
pce = GameBoard.pieces[t_sq];
}
}
for (index = 0; index < 8; index++) {
pce = GameBoard.pieces[sq + KiDir[index]];
if (
pce != SQUARES.OFFBOARD &&
PieceCol[pce] == side &&
PieceKing[pce] == true
) {
return true;
}
}
return false;
}
/****************************\
============================
Make Move
============================
\****************************/
function ClearPiece(sq) {
var pce = GameBoard.pieces[sq];
var col = PieceCol[pce];
var index;
var t_pceNum = -1;
hashPiece(pce, sq);
GameBoard.pieces[sq] = PIECES.EMPTY;
GameBoard.material[col] -= PieceVal[pce];
for (index = 0; index < GameBoard.pceNum[pce]; ++index) {
if (GameBoard.pList[getPieceIndex(pce, index)] == sq) {
t_pceNum = index;
break;
}
}
GameBoard.pceNum[pce]--;
GameBoard.pList[getPieceIndex(pce, t_pceNum)] =
GameBoard.pList[getPieceIndex(pce, GameBoard.pceNum[pce])];
}
function AddPiece(sq, pce) {
var col = PieceCol[pce];
hashPiece(pce, sq);
GameBoard.pieces[sq] = pce;
GameBoard.material[col] += PieceVal[pce];
GameBoard.pList[getPieceIndex(pce, GameBoard.pceNum[pce])] = sq;
GameBoard.pceNum[pce]++;
}
function MovePiece(from, to) {
var index = 0;
var pce = GameBoard.pieces[from];
hashPiece(pce, from);
GameBoard.pieces[from] = PIECES.EMPTY;
hashPiece(pce, to);
GameBoard.pieces[to] = pce;
for (index = 0; index < GameBoard.pceNum[pce]; ++index) {
if (GameBoard.pList[getPieceIndex(pce, index)] == from) {
GameBoard.pList[getPieceIndex(pce, index)] = to;
break;
}
}
}
function MakeMove(move) {
var from = fromSQ(move);
var to = toSQ(move);
var side = GameBoard.side;
GameBoard.history[GameBoard.hisPly].posKey = GameBoard.posKey;
if ((move & MFLAGEP) != 0) {
if (side == COLOURS.WHITE) {
ClearPiece(to - 10);
} else {
ClearPiece(to + 10);
}
} else if ((move & MFLAGCA) != 0) {
switch (to) {
case SQUARES.C1:
MovePiece(SQUARES.A1, SQUARES.D1);
break;
case SQUARES.C8:
MovePiece(SQUARES.A8, SQUARES.D8);
break;
case SQUARES.G1:
MovePiece(SQUARES.H1, SQUARES.F1);
break;
case SQUARES.G8:
MovePiece(SQUARES.H8, SQUARES.F8);
break;
default:
break;
}
}
if (GameBoard.enPas != SQUARES.NO_SQ) hashEnPas();
hashCastle();
GameBoard.history[GameBoard.hisPly].move = move;
GameBoard.history[GameBoard.hisPly].fiftyMove = GameBoard.fiftyMove;
GameBoard.history[GameBoard.hisPly].enPas = GameBoard.enPas;
GameBoard.history[GameBoard.hisPly].castlePerm = GameBoard.castlePerm;
GameBoard.castlePerm &= CastlePerm[from];
GameBoard.castlePerm &= CastlePerm[to];
GameBoard.enPas = SQUARES.NO_SQ;
hashCastle();
var captured = CAPTURED(move);
GameBoard.fiftyMove++;
if (captured != PIECES.EMPTY) {
ClearPiece(to);
GameBoard.fiftyMove = 0;
}
GameBoard.hisPly++;
GameBoard.ply++;
if (PiecePawn[GameBoard.pieces[from]] == true) {
GameBoard.fiftyMove = 0;
if ((move & MFLAGPS) != 0) {
if (side == COLOURS.WHITE) {
GameBoard.enPas = from + 10;
} else {
GameBoard.enPas = from - 10;
}
hashEnPas();
}
}
MovePiece(from, to);
var prPce = PROMOTED(move);
if (prPce != PIECES.EMPTY) {
ClearPiece(to);
AddPiece(to, prPce);
}
GameBoard.side ^= 1;
hashSide();
if (
SqAttacked(GameBoard.pList[getPieceIndex(Kings[side], 0)], GameBoard.side)
) {
TakeMove();
return false;
}
return true;
}
function TakeMove() {
GameBoard.hisPly--;
GameBoard.ply--;
var move = GameBoard.history[GameBoard.hisPly].move;
var from = fromSQ(move);
var to = toSQ(move);
if (GameBoard.enPas != SQUARES.NO_SQ) hashEnPas();
hashCastle();
GameBoard.castlePerm = GameBoard.history[GameBoard.hisPly].castlePerm;
GameBoard.fiftyMove = GameBoard.history[GameBoard.hisPly].fiftyMove;
GameBoard.enPas = GameBoard.history[GameBoard.hisPly].enPas;
if (GameBoard.enPas != SQUARES.NO_SQ) hashEnPas();
hashCastle();
GameBoard.side ^= 1;
hashSide();
if ((MFLAGEP & move) != 0) {
if (GameBoard.side == COLOURS.WHITE) {
AddPiece(to - 10, PIECES.bP);
} else {
AddPiece(to + 10, PIECES.wP);
}
} else if ((MFLAGCA & move) != 0) {
switch (to) {
case SQUARES.C1:
MovePiece(SQUARES.D1, SQUARES.A1);
break;
case SQUARES.C8:
MovePiece(SQUARES.D8, SQUARES.A8);
break;
case SQUARES.G1:
MovePiece(SQUARES.F1, SQUARES.H1);
break;
case SQUARES.G8:
MovePiece(SQUARES.F8, SQUARES.H8);
break;
default:
break;
}
}
MovePiece(to, from);
var captured = CAPTURED(move);
if (captured != PIECES.EMPTY) {
AddPiece(to, captured);
}
if (PROMOTED(move) != PIECES.EMPTY) {
ClearPiece(from);
AddPiece(
from,
PieceCol[PROMOTED(move)] == COLOURS.WHITE ? PIECES.wP : PIECES.bP
);
}
}
function ThreeFoldRep() {
var i = 0,
r = 0;
for (i = 0; i < GameBoard.hisPly; ++i) {
if (GameBoard.history[i].posKey == GameBoard.posKey) {
r++;
}
}
return r;
}
function DrawMaterial() {
if (GameBoard.pceNum[PIECES.wP] != 0 || GameBoard.pceNum[PIECES.bP] != 0)
return false;
if (
GameBoard.pceNum[PIECES.wQ] != 0 ||
GameBoard.pceNum[PIECES.bQ] != 0 ||
GameBoard.pceNum[PIECES.wR] != 0 ||
GameBoard.pceNum[PIECES.bR] != 0
)
return false;
if (GameBoard.pceNum[PIECES.wB] > 1 || GameBoard.pceNum[PIECES.bB] > 1) {
return false;
}
if (GameBoard.pceNum[PIECES.wN] > 1 || GameBoard.pceNum[PIECES.bN] > 1) {
return false;
}
if (GameBoard.pceNum[PIECES.wN] != 0 && GameBoard.pceNum[PIECES.wB] != 0) {
return false;
}
if (GameBoard.pceNum[PIECES.bN] != 0 && GameBoard.pceNum[PIECES.bB] != 0) {
return false;
}
return true;
}
/****************************\
============================
Move Gen
============================
\****************************/
var MvvLvaValue = [
0, 100, 200, 300, 400, 500, 600, 100, 200, 300, 400, 500, 600,
];
var MvvLvaScores = new Array(14 * 14);
function InitMvvLva() {
var Attacker;
var Victim;
for (Attacker = PIECES.wP; Attacker <= PIECES.bK; ++Attacker) {
for (Victim = PIECES.wP; Victim <= PIECES.bK; ++Victim) {
MvvLvaScores[Victim * 14 + Attacker] =
MvvLvaValue[Victim] + 6 - MvvLvaValue[Attacker] / 100;
}
}
}
function MoveExists(move) {
GenerateMoves();
var index;
var moveFound = NOMOVE;
for (
index = GameBoard.moveListStart[GameBoard.ply];
index < GameBoard.moveListStart[GameBoard.ply + 1];
++index
) {
moveFound = GameBoard.moveList[index];
if (MakeMove(moveFound) == false) {
continue;
}
TakeMove();
if (move == moveFound) {
return true;
}
}
return false;
}
function MOVE(from, to, captured, promoted, flag) {
return from | (to << 7) | (captured << 14) | (promoted << 20) | flag;
}
function AddCaptureMove(move) {
GameBoard.moveList[GameBoard.moveListStart[GameBoard.ply + 1]] = move;
GameBoard.moveScores[GameBoard.moveListStart[GameBoard.ply + 1]++] =
MvvLvaScores[CAPTURED(move) * 14 + GameBoard.pieces[fromSQ(move)]] +
1000000;
}
function AddQuietMove(move) {
GameBoard.moveList[GameBoard.moveListStart[GameBoard.ply + 1]] = move;
GameBoard.moveScores[GameBoard.moveListStart[GameBoard.ply + 1]] = 0;
if (move == GameBoard.searchKillers[GameBoard.ply]) {
GameBoard.moveScores[GameBoard.moveListStart[GameBoard.ply + 1]] = 900000;
} else if (move == GameBoard.searchKillers[GameBoard.ply + MAXDEPTH]) {
GameBoard.moveScores[GameBoard.moveListStart[GameBoard.ply + 1]] = 800000;
} else {
GameBoard.moveScores[GameBoard.moveListStart[GameBoard.ply + 1]] =
GameBoard.searchHistory[
GameBoard.pieces[fromSQ(move)] * BRD_SQ_NUM + toSQ(move)
];
}
GameBoard.moveListStart[GameBoard.ply + 1]++;
}
function AddEnPassantMove(move) {
GameBoard.moveList[GameBoard.moveListStart[GameBoard.ply + 1]] = move;
GameBoard.moveScores[GameBoard.moveListStart[GameBoard.ply + 1]++] =
105 + 1000000;
}
function AddWhitePawnCaptureMove(from, to, cap) {
if (RanksBrd[from] == RANKS.RANK_7) {
AddCaptureMove(MOVE(from, to, cap, PIECES.wQ, 0));
AddCaptureMove(MOVE(from, to, cap, PIECES.wR, 0));
AddCaptureMove(MOVE(from, to, cap, PIECES.wB, 0));
AddCaptureMove(MOVE(from, to, cap, PIECES.wN, 0));
} else {
AddCaptureMove(MOVE(from, to, cap, PIECES.EMPTY, 0));
}
}
function AddBlackPawnCaptureMove(from, to, cap) {
if (RanksBrd[from] == RANKS.RANK_2) {
AddCaptureMove(MOVE(from, to, cap, PIECES.bQ, 0));
AddCaptureMove(MOVE(from, to, cap, PIECES.bR, 0));
AddCaptureMove(MOVE(from, to, cap, PIECES.bB, 0));
AddCaptureMove(MOVE(from, to, cap, PIECES.bN, 0));
} else {
AddCaptureMove(MOVE(from, to, cap, PIECES.EMPTY, 0));
}
}
function AddWhitePawnQuietMove(from, to) {
if (RanksBrd[from] == RANKS.RANK_7) {
AddQuietMove(MOVE(from, to, PIECES.EMPTY, PIECES.wQ, 0));
AddQuietMove(MOVE(from, to, PIECES.EMPTY, PIECES.wR, 0));
AddQuietMove(MOVE(from, to, PIECES.EMPTY, PIECES.wB, 0));
AddQuietMove(MOVE(from, to, PIECES.EMPTY, PIECES.wN, 0));
} else {
AddQuietMove(MOVE(from, to, PIECES.EMPTY, PIECES.EMPTY, 0));
}
}
function AddBlackPawnQuietMove(from, to) {
if (RanksBrd[from] == RANKS.RANK_2) {
AddQuietMove(MOVE(from, to, PIECES.EMPTY, PIECES.bQ, 0));
AddQuietMove(MOVE(from, to, PIECES.EMPTY, PIECES.bR, 0));
AddQuietMove(MOVE(from, to, PIECES.EMPTY, PIECES.bB, 0));
AddQuietMove(MOVE(from, to, PIECES.EMPTY, PIECES.bN, 0));
} else {
AddQuietMove(MOVE(from, to, PIECES.EMPTY, PIECES.EMPTY, 0));
}
}
function GenerateMoves() {
GameBoard.moveListStart[GameBoard.ply + 1] =
GameBoard.moveListStart[GameBoard.ply];
var pceType;
var pceNum;
var sq;
var pceIndex;
var pce;
var t_sq;
var dir;
if (GameBoard.side == COLOURS.WHITE) {
pceType = PIECES.wP;
for (pceNum = 0; pceNum < GameBoard.pceNum[pceType]; ++pceNum) {
sq = GameBoard.pList[getPieceIndex(pceType, pceNum)];
if (GameBoard.pieces[sq + 10] == PIECES.EMPTY) {
AddWhitePawnQuietMove(sq, sq + 10);
if (
RanksBrd[sq] == RANKS.RANK_2 &&
GameBoard.pieces[sq + 20] == PIECES.EMPTY
) {
AddQuietMove(
MOVE(sq, sq + 20, PIECES.EMPTY, PIECES.EMPTY, MFLAGPS)
);
}
}
if (
isSqOffBoard(sq + 9) == false &&
PieceCol[GameBoard.pieces[sq + 9]] == COLOURS.BLACK
) {
AddWhitePawnCaptureMove(sq, sq + 9, GameBoard.pieces[sq + 9]);
}
if (
isSqOffBoard(sq + 11) == false &&
PieceCol[GameBoard.pieces[sq + 11]] == COLOURS.BLACK
) {
AddWhitePawnCaptureMove(sq, sq + 11, GameBoard.pieces[sq + 11]);
}
if (GameBoard.enPas != SQUARES.NO_SQ) {
if (sq + 9 == GameBoard.enPas) {
AddEnPassantMove(
MOVE(sq, sq + 9, PIECES.EMPTY, PIECES.EMPTY, MFLAGEP)
);
}
if (sq + 11 == GameBoard.enPas) {
AddEnPassantMove(
MOVE(sq, sq + 11, PIECES.EMPTY, PIECES.EMPTY, MFLAGEP)
);
}
}
}
if (GameBoard.castlePerm & CASTLEBIT.whiteKing) {
if (
GameBoard.pieces[SQUARES.F1] == PIECES.EMPTY &&
GameBoard.pieces[SQUARES.G1] == PIECES.EMPTY
) {
if (
SqAttacked(SQUARES.F1, COLOURS.BLACK) == false &&
SqAttacked(SQUARES.E1, COLOURS.BLACK) == false
) {
AddQuietMove(
MOVE(SQUARES.E1, SQUARES.G1, PIECES.EMPTY, PIECES.EMPTY, MFLAGCA)
);
}
}
}
if (GameBoard.castlePerm & CASTLEBIT.whiteQueen) {
if (
GameBoard.pieces[SQUARES.D1] == PIECES.EMPTY &&
GameBoard.pieces[SQUARES.C1] == PIECES.EMPTY &&
GameBoard.pieces[SQUARES.B1] == PIECES.EMPTY
) {
if (
SqAttacked(SQUARES.D1, COLOURS.BLACK) == false &&
SqAttacked(SQUARES.E1, COLOURS.BLACK) == false
) {
AddQuietMove(
MOVE(SQUARES.E1, SQUARES.C1, PIECES.EMPTY, PIECES.EMPTY, MFLAGCA)
);
}
}
}
} else {
pceType = PIECES.bP;
for (pceNum = 0; pceNum < GameBoard.pceNum[pceType]; ++pceNum) {
sq = GameBoard.pList[getPieceIndex(pceType, pceNum)];
if (GameBoard.pieces[sq - 10] == PIECES.EMPTY) {
AddBlackPawnQuietMove(sq, sq - 10);
if (
RanksBrd[sq] == RANKS.RANK_7 &&
GameBoard.pieces[sq - 20] == PIECES.EMPTY
) {
AddQuietMove(
MOVE(sq, sq - 20, PIECES.EMPTY, PIECES.EMPTY, MFLAGPS)
);
}
}
if (
isSqOffBoard(sq - 9) == false &&
PieceCol[GameBoard.pieces[sq - 9]] == COLOURS.WHITE
) {
AddBlackPawnCaptureMove(sq, sq - 9, GameBoard.pieces[sq - 9]);
}
if (
isSqOffBoard(sq - 11) == false &&
PieceCol[GameBoard.pieces[sq - 11]] == COLOURS.WHITE
) {
AddBlackPawnCaptureMove(sq, sq - 11, GameBoard.pieces[sq - 11]);
}
if (GameBoard.enPas != SQUARES.NO_SQ) {
if (sq - 9 == GameBoard.enPas) {
AddEnPassantMove(
MOVE(sq, sq - 9, PIECES.EMPTY, PIECES.EMPTY, MFLAGEP)
);
}
if (sq - 11 == GameBoard.enPas) {
AddEnPassantMove(
MOVE(sq, sq - 11, PIECES.EMPTY, PIECES.EMPTY, MFLAGEP)
);
}
}
}
if (GameBoard.castlePerm & CASTLEBIT.blackKing) {
if (
GameBoard.pieces[SQUARES.F8] == PIECES.EMPTY &&
GameBoard.pieces[SQUARES.G8] == PIECES.EMPTY
) {
if (
SqAttacked(SQUARES.F8, COLOURS.WHITE) == false &&
SqAttacked(SQUARES.E8, COLOURS.WHITE) == false
) {
AddQuietMove(
MOVE(SQUARES.E8, SQUARES.G8, PIECES.EMPTY, PIECES.EMPTY, MFLAGCA)
);
}
}
}
if (GameBoard.castlePerm & CASTLEBIT.blackQueen) {
if (
GameBoard.pieces[SQUARES.D8] == PIECES.EMPTY &&
GameBoard.pieces[SQUARES.C8] == PIECES.EMPTY &&
GameBoard.pieces[SQUARES.B8] == PIECES.EMPTY
) {
if (
SqAttacked(SQUARES.D8, COLOURS.WHITE) == false &&
SqAttacked(SQUARES.E8, COLOURS.WHITE) == false
) {
AddQuietMove(
MOVE(SQUARES.E8, SQUARES.C8, PIECES.EMPTY, PIECES.EMPTY, MFLAGCA)
);
}
}
}
}
pceIndex = LoopNonSlideIndex[GameBoard.side];
pce = LoopNonSlidePce[pceIndex++];
while (pce != 0) {
for (pceNum = 0; pceNum < GameBoard.pceNum[pce]; ++pceNum) {
sq = GameBoard.pList[getPieceIndex(pce, pceNum)];
for (index = 0; index < DirNum[pce]; index++) {
dir = PceDir[pce][index];
t_sq = sq + dir;
if (isSqOffBoard(t_sq) == true) {
continue;
}
if (GameBoard.pieces[t_sq] != PIECES.EMPTY) {
if (PieceCol[GameBoard.pieces[t_sq]] != GameBoard.side) {
AddCaptureMove(
MOVE(sq, t_sq, GameBoard.pieces[t_sq], PIECES.EMPTY, 0)
);
}
} else {
AddQuietMove(MOVE(sq, t_sq, PIECES.EMPTY, PIECES.EMPTY, 0));
}
}
}
pce = LoopNonSlidePce[pceIndex++];
}
pceIndex = LoopSlideIndex[GameBoard.side];
pce = LoopSlidePce[pceIndex++];
while (pce != 0) {
for (pceNum = 0; pceNum < GameBoard.pceNum[pce]; ++pceNum) {
sq = GameBoard.pList[getPieceIndex(pce, pceNum)];
for (index = 0; index < DirNum[pce]; index++) {
dir = PceDir[pce][index];
t_sq = sq + dir;
while (isSqOffBoard(t_sq) == false) {
if (GameBoard.pieces[t_sq] != PIECES.EMPTY) {
if (PieceCol[GameBoard.pieces[t_sq]] != GameBoard.side) {
AddCaptureMove(
MOVE(sq, t_sq, GameBoard.pieces[t_sq], PIECES.EMPTY, 0)
);
}
break;
}
AddQuietMove(MOVE(sq, t_sq, PIECES.EMPTY, PIECES.EMPTY, 0));
t_sq += dir;
}
}
}
pce = LoopSlidePce[pceIndex++];
}
}
function GenerateCaptures() {
GameBoard.moveListStart[GameBoard.ply + 1] =
GameBoard.moveListStart[GameBoard.ply];
var pceType;
var pceNum;
var sq;
var pceIndex;
var pce;
var t_sq;
var dir;
if (GameBoard.side == COLOURS.WHITE) {
pceType = PIECES.wP;
for (pceNum = 0; pceNum < GameBoard.pceNum[pceType]; ++pceNum) {
sq = GameBoard.pList[getPieceIndex(pceType, pceNum)];
if (
isSqOffBoard(sq + 9) == false &&
PieceCol[GameBoard.pieces[sq + 9]] == COLOURS.BLACK
) {
AddWhitePawnCaptureMove(sq, sq + 9, GameBoard.pieces[sq + 9]);
}
if (
isSqOffBoard(sq + 11) == false &&
PieceCol[GameBoard.pieces[sq + 11]] == COLOURS.BLACK
) {
AddWhitePawnCaptureMove(sq, sq + 11, GameBoard.pieces[sq + 11]);
}
if (GameBoard.enPas != SQUARES.NO_SQ) {
if (sq + 9 == GameBoard.enPas) {
AddEnPassantMove(
MOVE(sq, sq + 9, PIECES.EMPTY, PIECES.EMPTY, MFLAGEP)
);
}
if (sq + 11 == GameBoard.enPas) {
AddEnPassantMove(
MOVE(sq, sq + 11, PIECES.EMPTY, PIECES.EMPTY, MFLAGEP)
);
}
}
}
} else {
pceType = PIECES.bP;
for (pceNum = 0; pceNum < GameBoard.pceNum[pceType]; ++pceNum) {
sq = GameBoard.pList[getPieceIndex(pceType, pceNum)];
if (
isSqOffBoard(sq - 9) == false &&
PieceCol[GameBoard.pieces[sq - 9]] == COLOURS.WHITE
) {
AddBlackPawnCaptureMove(sq, sq - 9, GameBoard.pieces[sq - 9]);
}
if (
isSqOffBoard(sq - 11) == false &&
PieceCol[GameBoard.pieces[sq - 11]] == COLOURS.WHITE
) {
AddBlackPawnCaptureMove(sq, sq - 11, GameBoard.pieces[sq - 11]);
}
if (GameBoard.enPas != SQUARES.NO_SQ) {
if (sq - 9 == GameBoard.enPas) {
AddEnPassantMove(
MOVE(sq, sq - 9, PIECES.EMPTY, PIECES.EMPTY, MFLAGEP)
);
}
if (sq - 11 == GameBoard.enPas) {
AddEnPassantMove(
MOVE(sq, sq - 11, PIECES.EMPTY, PIECES.EMPTY, MFLAGEP)
);
}
}
}
}
pceIndex = LoopNonSlideIndex[GameBoard.side];
pce = LoopNonSlidePce[pceIndex++];
while (pce != 0) {
for (pceNum = 0; pceNum < GameBoard.pceNum[pce]; ++pceNum) {
sq = GameBoard.pList[getPieceIndex(pce, pceNum)];
for (index = 0; index < DirNum[pce]; index++) {
dir = PceDir[pce][index];
t_sq = sq + dir;
if (isSqOffBoard(t_sq) == true) {
continue;
}
if (GameBoard.pieces[t_sq] != PIECES.EMPTY) {
if (PieceCol[GameBoard.pieces[t_sq]] != GameBoard.side) {
AddCaptureMove(
MOVE(sq, t_sq, GameBoard.pieces[t_sq], PIECES.EMPTY, 0)
);
}
}
}
}
pce = LoopNonSlidePce[pceIndex++];
}
pceIndex = LoopSlideIndex[GameBoard.side];
pce = LoopSlidePce[pceIndex++];
while (pce != 0) {
for (pceNum = 0; pceNum < GameBoard.pceNum[pce]; ++pceNum) {
sq = GameBoard.pList[getPieceIndex(pce, pceNum)];
for (index = 0; index < DirNum[pce]; index++) {
dir = PceDir[pce][index];
t_sq = sq + dir;
while (isSqOffBoard(t_sq) == false) {
if (GameBoard.pieces[t_sq] != PIECES.EMPTY) {
if (PieceCol[GameBoard.pieces[t_sq]] != GameBoard.side) {
AddCaptureMove(
MOVE(sq, t_sq, GameBoard.pieces[t_sq], PIECES.EMPTY, 0)
);
}
break;
}
t_sq += dir;
}
}
}
pce = LoopSlidePce[pceIndex++];
}
}
/****************************\
============================
Evaluation
============================
\****************************/
// prettier-ignore
{
var mg_value = {
wP: 82, bP: 82,
wN: 337, bN: 337,
wB: 365, bB: 365,
wR: 477, bR: 477,
wQ: 1025, bQ: 1025,
wK: 50000, bK: 50000,
}
var eg_value = {
wP: 94, bP: 94,
wN: 281, bN: 281,
wB: 297, bB: 297,
wR: 512, bR: 512,
wQ: 936, bQ: 936,
wK: 50000, bK: 50000,
}
mg_pawn_table = [
0, 0, 0, 0, 0, 0, 0, 0,
98, 134, 61, 95, 68, 126, 34, -11,
-6, 7, 26, 31, 65, 56, 25, -20,
-14, 13, 6, 21, 23, 12, 17, -23,
-27, -2, -5, 12, 17, 6, 10, -25,
-26, -4, -4, -10, 3, 3, 33, -12,
-35, -1, -20, -23, -15, 24, 38, -22,
0, 0, 0, 0, 0, 0, 0, 0,
];
eg_pawn_table = [
0, 0, 0, 0, 0, 0, 0, 0,
178, 173, 158, 134, 147, 132, 165, 187,
94, 100, 85, 67, 56, 53, 82, 84,
32, 24, 13, 5, -2, 4, 17, 17,
13, 9, -3, -7, -7, -8, 3, -1,
4, 7, -6, 1, 0, -5, -1, -8,
13, 8, 8, 10, 13, 0, 2, -7,
0, 0, 0, 0, 0, 0, 0, 0,
];
mg_knight_table = [
-167, -89, -34, -49, 61, -97, -15, -107,
-73, -41, 72, 36, 23, 62, 7, -17,
-47, 60, 37, 65, 84, 129, 73, 44,
-9, 17, 19, 53, 37, 69, 18, 22,
-13, 4, 16, 13, 28, 19, 21, -8,
-23, -9, 12, 10, 19, 17, 25, -16,
-29, -53, -12, -3, -1, 18, -14, -19,
-105, -21, -58, -33, -17, -28, -19, -23,
];
eg_knight_table = [
-58, -38, -13, -28, -31, -27, -63, -99,
-25, -8, -25, -2, -9, -25, -24, -52,
-24, -20, 10, 9, -1, -9, -19, -41,
-17, 3, 22, 22, 22, 11, 8, -18,
-18, -6, 16, 25, 16, 17, 4, -18,
-23, -3, -1, 15, 10, -3, -20, -22,
-42, -20, -10, -5, -2, -20, -23, -44,
-29, -51, -23, -15, -22, -18, -50, -64,
];
mg_bishop_table = [
-29, 4, -82, -37, -25, -42, 7, -8,
-26, 16, -18, -13, 30, 59, 18, -47,
-16, 37, 43, 40, 35, 50, 37, -2,
-4, 5, 19, 50, 37, 37, 7, -2,
-6, 13, 13, 26, 34, 12, 10, 4,
0, 15, 15, 15, 14, 27, 18, 10,
4, 15, 16, 0, 7, 21, 33, 1,
-33, -3, -14, -21, -13, -12, -39, -21,
];
eg_bishop_table = [
-14, -21, -11, -8, -7, -9, -17, -24,
-8, -4, 7, -12, -3, -13, -4, -14,
2, -8, 0, -1, -2, 6, 0, 4,
-3, 9, 12, 9, 14, 10, 3, 2,
-6, 3, 13, 19, 7, 10, -3, -9,
-12, -3, 8, 10, 13, 3, -7, -15,
-14, -18, -7, -1, 4, -9, -15, -27,
-23, -9, -23, -5, -9, -16, -5, -17,
];
mg_rook_table = [
32, 42, 32, 51, 63, 9, 31, 43,
27, 32, 58, 62, 80, 67, 26, 44,
-5, 19, 26, 36, 17, 45, 61, 16,
-24, -11, 7, 26, 24, 35, -8, -20,
-36, -26, -12, -1, 9, -7, 6, -23,
-45, -25, -16, -17, 3, 0, -5, -33,
-44, -16, -20, -9, -1, 11, -6, -71,
-19, -13, 1, 17, 16, 7, -37, -26,
];
eg_rook_table = [
13, 10, 18, 15, 12, 12, 8, 5,
11, 13, 13, 11, -3, 3, 8, 3,
7, 7, 7, 5, 4, -3, -5, -3,
4, 3, 13, 1, 2, 1, -1, 2,
3, 5, 8, 4, -5, -6, -8, -11,
-4, 0, -5, -1, -7, -12, -8, -16,
-6, -6, 0, 2, -9, -9, -11, -3,
-9, 2, 3, -1, -5, -13, 4, -20,
];
mg_queen_table = [
-28, 0, 29, 12, 59, 44, 43, 45,
-24, -39, -5, 1, -16, 57, 28, 54,
-13, -17, 7, 8, 29, 56, 47, 57,
-27, -27, -16, -16, -1, 17, -2, 1,
-9, -26, -9, -10, -2, -4, 3, -3,
-14, 2, -11, -2, -5, 2, 14, 5,
-35, -8, 11, 2, 8, 15, -3, 1,
-1, -18, -9, 10, -15, -25, -31, -50,
];
eg_queen_table = [
-9, 22, 22, 27, 27, 19, 10, 20,
-17, 20, 32, 41, 58, 25, 30, 0,
-20, 6, 9, 49, 47, 35, 19, 9,
3, 22, 24, 45, 57, 40, 57, 36,
-18, 28, 19, 47, 31, 34, 39, 23,
-16, -27, 15, 6, 9, 17, 10, 5,
-22, -23, -30, -16, -16, -23, -36, -32,
-33, -28, -22, -43, -5, -32, -20, -41,
];
mg_king_table = [
-65, 23, 16, -15, -56, -34, 2, 13,
29, -1, -20, -7, -8, -4, -38, -29,
-9, 24, 2, -16, -20, 6, 22, -22,
-17, -20, -12, -27, -30, -25, -14, -36,
-49, -1, -27, -39, -46, -44, -33, -51,
-14, -14, -22, -46, -44, -30, -15, -27,
1, 7, -8, -64, -43, -16, 9, 8,
-15, 36, 12, -54, 8, -28, 24, 14,
];
eg_king_table = [
-74, -35, -18, -18, -11, 15, 4, -17,
-12, 17, 14, 17, 17, 38, 23, 11,
10, 17, 23, 15, 20, 45, 44, 13,
-8, 22, 24, 27, 26, 33, 26, 3,
-18, -4, 21, 24, 27, 23, 9, -11,
-19, -3, 11, 21, 23, 16, 7, -9,
-27, -11, 4, 13, 14, 4, -5, -17,
-53, -34, -21, -11, -28, -14, -24, -43
];
}
const BishopPair = 40;
var mg_pesto_table = {
wP: mg_pawn_table,
bP: mg_pawn_table,
wN: mg_knight_table,
bN: mg_knight_table,
wB: mg_bishop_table,
bB: mg_bishop_table,
wR: mg_rook_table,
bR: mg_rook_table,
wQ: mg_queen_table,
bQ: mg_queen_table,
wK: mg_king_table,
bK: mg_king_table,
};
var eg_pesto_table = {
wP: eg_pawn_table,
bP: eg_pawn_table,
wN: eg_knight_table,
bN: eg_knight_table,
wB: eg_bishop_table,
bB: eg_bishop_table,
wR: eg_rook_table,
bR: eg_rook_table,
wQ: eg_queen_table,
bQ: eg_queen_table,
wK: eg_king_table,
bK: eg_king_table,
};
// wP - bP - wN - bN - wB - bB - wR - bR - wQ - bQ - wK - bK
var gamephaseInc = {
wP: 0,
bP: 0,
wN: 1,
bN: 1,
wB: 1,
bB: 1,
wR: 2,
bR: 2,
wQ: 4,
bQ: 4,
wK: 0,
bK: 0,
};
// initTables()
function EvalPosition() {
let gamePhase = 0;
// var score =
// GameBoard.material[COLOURS.WHITE] - GameBoard.material[COLOURS.BLACK];
let mg_score = 0;
let eg_score = 0;
for (pce in PIECES) {
for (pceNum = 0; pceNum < GameBoard.pceNum[PIECES[pce]]; pceNum++) {
sq = GameBoard.pList[getPieceIndex(PIECES[pce], pceNum)];
if (pce[0] == "w") {
// score += table[pce][sq120to64(sq)];
mg_score +=
mg_value[pce] + mg_pesto_table[pce][mirror64(sq120to64(sq))];
eg_score +=
eg_value[pce] + eg_pesto_table[pce][mirror64(sq120to64(sq))];
gamePhase += gamephaseInc[pce];
} else {
// score -= table[pce][(mirror64(sq120to64(sq)))];
mg_score -= mg_value[pce] + mg_pesto_table[pce][sq120to64(sq)];
eg_score -= eg_value[pce] + eg_pesto_table[pce][sq120to64(sq)];
gamePhase += gamephaseInc[pce];
}
}
}
mg_phase = gamePhase;
eg_phase = 24 - gamePhase;
score = (mg_score * mg_phase + eg_score * eg_phase) / 24;
if (GameBoard.side == COLOURS.WHITE) {
return score;
} else {
return -score;
}
}
/****************************\
============================
Search
============================
\****************************/
function GetPvLine(depth) {
var move = ProbePvTable();
var count = 0;
while (move != NOMOVE && count < depth) {
if (MoveExists(move) == true) {
MakeMove(move);
GameBoard.PvArray[count++] = move;
} else {
break;
}
move = ProbePvTable();
}
while (GameBoard.ply > 0) {
TakeMove();
}
return count;
}
function ProbePvTable() {
var index = GameBoard.posKey % PVENTRIES;
if (GameBoard.PvTable[index].posKey == GameBoard.posKey) {
return GameBoard.PvTable[index].move;
}
return NOMOVE;
}
function StorePvMove(move) {
var index = GameBoard.posKey % PVENTRIES;
GameBoard.PvTable[index].posKey = GameBoard.posKey;
GameBoard.PvTable[index].move = move;
}
var SearchController = {};
SearchController.nodes;
SearchController.fh;
SearchController.fhf;
SearchController.depth;
SearchController.time = 1000;
SearchController.start;
SearchController.stop;
SearchController.best;
SearchController.thinking;
SearchController.endgame;
function PickNextMove(MoveNum) {
var index = 0;
var bestScore = -1;
var bestNum = MoveNum;
for (
index = MoveNum;
index < GameBoard.moveListStart[GameBoard.ply + 1];
++index
) {
if (GameBoard.moveScores[index] > bestScore) {
bestScore = GameBoard.moveScores[index];
bestNum = index;
}
}
if (bestNum != MoveNum) {
var temp = 0;
temp = GameBoard.moveScores[MoveNum];
GameBoard.moveScores[MoveNum] = GameBoard.moveScores[bestNum];
GameBoard.moveScores[bestNum] = temp;
temp = GameBoard.moveList[MoveNum];
GameBoard.moveList[MoveNum] = GameBoard.moveList[bestNum];
GameBoard.moveList[bestNum] = temp;
}
}
function ClearPvTable() {
for (index = 0; index < PVENTRIES; index++) {
GameBoard.PvTable[index].move = NOMOVE;
GameBoard.PvTable[index].posKey = 0;
}
}
function CheckUp() {
if (performance.now() - SearchController.start > SearchController.time) {
SearchController.stop = true;
}
}
function IsRepetition() {
var index = 0;
for (
index = GameBoard.hisPly - GameBoard.fiftyMove;
index < GameBoard.hisPly - 1;
++index
) {
if (GameBoard.posKey == GameBoard.history[index].posKey) {
return true;
}
}
return false;
}
function Quiescence(alpha, beta) {
if ((SearchController.nodes & 2047) == 0) {
CheckUp();
}
SearchController.nodes++;
if ((IsRepetition() || GameBoard.fiftyMove >= 100) && GameBoard.ply != 0) {
return 0;
}
if (GameBoard.ply > MAXDEPTH - 1) {
return EvalPosition();
}
var Score = EvalPosition();
if (Score >= beta) {
return beta;
}
if (Score > alpha) {
alpha = Score;
}
GenerateCaptures();
var MoveNum = 0;
var Legal = 0;
var OldAlpha = alpha;
var BestMove = NOMOVE;
var Move = NOMOVE;
for (
MoveNum = GameBoard.moveListStart[GameBoard.ply];
MoveNum < GameBoard.moveListStart[GameBoard.ply + 1];
++MoveNum
) {
PickNextMove(MoveNum);
Move = GameBoard.moveList[MoveNum];
if (MakeMove(Move) == false) {
continue;
}
Legal++;
Score = -Quiescence(-beta, -alpha);
TakeMove();
if (SearchController.stop == true) {
return 0;
}
if (Score > alpha) {
if (Score >= beta) {
if (Legal == 1) {
SearchController.fhf++;
}
SearchController.fh++;
return beta;
}
alpha = Score;
BestMove = Move;
}
}
if (alpha != OldAlpha) {
StorePvMove(BestMove);
}
return alpha;
}
function AlphaBeta(alpha, beta, depth) {
if (depth <= 0) {
return Quiescence(alpha, beta);
}
if ((SearchController.nodes & 2047) == 0) {
CheckUp();
}
SearchController.nodes++;
if ((IsRepetition() || GameBoard.fiftyMove >= 100) && GameBoard.ply != 0) {
return 0;
}
if (GameBoard.ply > MAXDEPTH - 1) {
return EvalPosition();
}
var InCheck = SqAttacked(
GameBoard.pList[getPieceIndex(Kings[GameBoard.side], 0)],
GameBoard.side ^ 1
);
if (InCheck == true) {
depth++;
}
var Score = -INFINITE;
GenerateMoves();
var MoveNum = 0;
var Legal = 0;
var OldAlpha = alpha;
var BestMove = NOMOVE;
var Move = NOMOVE;
var PvMove = ProbePvTable();
if (PvMove != NOMOVE) {
for (
MoveNum = GameBoard.moveListStart[GameBoard.ply];
MoveNum < GameBoard.moveListStart[GameBoard.ply + 1];
++MoveNum
) {
if (GameBoard.moveList[MoveNum] == PvMove) {
GameBoard.moveScores[MoveNum] = 2000000;
break;
}
}
}
for (
MoveNum = GameBoard.moveListStart[GameBoard.ply];
MoveNum < GameBoard.moveListStart[GameBoard.ply + 1];
++MoveNum
) {
PickNextMove(MoveNum);
Move = GameBoard.moveList[MoveNum];
if (MakeMove(Move) == false) {
continue;
}
Legal++;
Score = -AlphaBeta(-beta, -alpha, depth - 1);
TakeMove();
if (SearchController.stop == true) {
return 0;
}
if (Score > alpha) {
if (Score >= beta) {
if (Legal == 1) {
SearchController.fhf++;
}
SearchController.fh++;
if ((Move & MFLAGCAP) == 0) {
GameBoard.searchKillers[MAXDEPTH + GameBoard.ply] =
GameBoard.searchKillers[GameBoard.ply];
GameBoard.searchKillers[GameBoard.ply] = Move;
}
return beta;
}
if ((Move & MFLAGCAP) == 0) {
GameBoard.searchHistory[
GameBoard.pieces[fromSQ(Move)] * BRD_SQ_NUM + toSQ(Move)
] += depth * depth;
}
alpha = Score;
BestMove = Move;
}
}
if (Legal == 0) {
if (InCheck == true) {
return -MATE + GameBoard.ply;
} else {
return 0;
}
}
if (alpha != OldAlpha) {
StorePvMove(BestMove);
}
return alpha;
}
function CheckEndgame() {
totalMaterial =
GameBoard.material[COLOURS.WHITE] + GameBoard.material[COLOURS.BLACK];
if (totalMaterial < 105000) {
SearchController.endgame = true;
} else {
SearchController.endgame = false;
}
}
function ClearForSearch() {
var index = 0;
for (index = 0; index < 14 * BRD_SQ_NUM; ++index) {
GameBoard.searchHistory[index] = 0;
}
for (index = 0; index < 3 * MAXDEPTH; ++index) {
GameBoard.searchKillers[index] = 0;
}
ClearPvTable();
CheckEndgame();
GameBoard.ply = 0;
SearchController.nodes = 0;
SearchController.fh = 0;
SearchController.fhf = 0;
SearchController.start = performance.now();
SearchController.stop = false;
}
function SearchPosition() {
var bestMove = NOMOVE;
var bestScore = -INFINITE;
var currentDepth = 0;
var line;
var PvNum;
var c;
ClearForSearch();
for (
currentDepth = 1;
currentDepth <= SearchController.depth;
++currentDepth
) {
bestScore = AlphaBeta(-INFINITE, INFINITE, currentDepth);
if (SearchController.stop) {
break;
}
bestMove = ProbePvTable();
line =
"D:" +
currentDepth +
" Best:" +
PrMove(bestMove) +
" Score:" +
bestScore +
" nodes:" +
SearchController.nodes;
PvNum = GetPvLine(currentDepth);
line += " Pv:";
for (c = 0; c < PvNum; ++c) {
line += " " + PrMove(GameBoard.PvArray[c]);
}
if (currentDepth != 1) {
line +=
" Ordering:" +
((SearchController.fhf / SearchController.fh) * 100).toFixed(2) +
"%";
}
// console.log(line);
}
SearchController.best = bestMove;
SearchController.thinking = false;
}
function getBestMove() {
SearchController.depth = MAXDEPTH;
SearchPosition();
return SearchController.best;
}
/****************************\
============================
Perft Test
============================
\****************************/
var perft_leafNodes;
function Perft(depth) {
if (depth == 0) {
perft_leafNodes++;
return;
}
GenerateMoves();
var index;
var move;
for (
index = GameBoard.moveListStart[GameBoard.ply];
index < GameBoard.moveListStart[GameBoard.ply + 1];
++index
) {
move = GameBoard.moveList[index];
if (MakeMove(move) == false) {
continue;
}
Perft(depth - 1);
TakeMove();
}
return;
}
function PerftTest(depth) {
PrintBoard();
console.log("Starting Test To Depth:" + depth);
perft_leafNodes = 0;
GenerateMoves();
var index;
var move;
var moveNum = 0;
for (
index = GameBoard.moveListStart[GameBoard.ply];
index < GameBoard.moveListStart[GameBoard.ply + 1];
++index
) {
move = GameBoard.moveList[index];
if (MakeMove(move) == false) {
continue;
}
moveNum++;
var cumnodes = perft_leafNodes;
Perft(depth - 1);
TakeMove();
var oldnodes = perft_leafNodes - cumnodes;
console.log("move:" + moveNum + " " + PrMove(move) + " " + oldnodes);
}
console.log("Test Complete : " + perft_leafNodes + " leaf nodes visited");
return;
}
function PrMove(move) {
var MvStr;
var ff = FilesBrd[fromSQ(move)];
var rf = RanksBrd[fromSQ(move)];
var ft = FilesBrd[toSQ(move)];
var rt = RanksBrd[toSQ(move)];
MvStr = FileChar[ff] + RankChar[rf] + FileChar[ft] + RankChar[rt];
var promoted = PROMOTED(move);
if (promoted != PIECES.EMPTY) {
var pchar = "q";
if (PieceKnight[promoted] == true) {
pchar = "n";
} else if (
PieceRookQueen[promoted] == true &&
PieceBishopQueen[promoted] == false
) {
pchar = "r";
} else if (
PieceRookQueen[promoted] == false &&
PieceBishopQueen[promoted] == true
) {
pchar = "b";
}
MvStr += pchar;
}
return MvStr;
}
function getMoveList() {
GenerateMoves();
var index;
var move;
moves = [];
for (
index = GameBoard.moveListStart[GameBoard.ply];
index < GameBoard.moveListStart[GameBoard.ply + 1];
++index
) {
move = GameBoard.moveList[index];
from = fromSQ(move);
to = toSQ(move);
const parsed = ParseMove(from, to);
if (parsed !== NOMOVE) {
moves.push(PrMove(move).slice(0, 4));
}
}
return moves;
}
// LETS GO INITALISE!!!
init();
/****************************\
============================
Public API Methods
============================
\****************************/
function getMovesAtSquare(square) {
const allMoves = getMoveList();
const movesAtSquare = allMoves.filter((move) =>
move.slice(0, 2).includes(square)
);
const possibleMoves = movesAtSquare.map((move) => move.slice(2, 4));
return possibleMoves;
}
function move(from, to) {
from = OppositePrSq(from);
to = OppositePrSq(to);
const parsed = ParseMove(from, to);
if (parsed == NOMOVE) return false;
else {
MakeMove(parsed);
return true;
}
}
function makeAIMove() {
if (gameStatus().over) return false;
let bestMove = getBestMove();
MakeMove(bestMove);
}
function reset() {
ParseFen(START_FEN);
}
function gameStatus() {
const sideToMove = GameBoard.side == COLOURS.WHITE ? "white" : "black";
let over = false;
if (GameBoard.fiftyMove >= 100) over = "Game is a draw by 50 move rule";
if (ThreeFoldRep() >= 2) over = "Game drawn by threefold repetition";
if (DrawMaterial()) over = "Game drawn by insufficient material";
moves = getMoveList();
let check = SqAttacked(
GameBoard.pList[getPieceIndex(Kings[GameBoard.side], 0)],
GameBoard.side ^ 1
);
if (moves.length === 0) {
if (check) {
over = "Checkmate!";
} else {
over = "Game drawn by stalemate";
}
}
return { over: over, sideToMove: sideToMove, check: check };
}
function setThinkingTime(time) {
SearchController.time = time * 1000;
}
/****************************\
============================
Public API
============================
\****************************/
return {
getFEN: GenerateFEN,
setFEN: ParseFen,
getMovesAtSquare: getMovesAtSquare,
move: move,
makeAIMove: makeAIMove,
getBestMove: getBestMove,
reset: reset,
gameStatus: gameStatus,
setThinkingTime: setThinkingTime,
};
};
const vasara = function () {
const mousePosition = { x: 0, y: 0 };
const mouseButtons = new Map();
document.addEventListener('mousemove', (event) => {
mousePosition.x = event.clientX;
mousePosition.y = event.clientY;
});
document.addEventListener('mousedown', (event) => {
mouseButtons.set(event.button, true);
});
document.addEventListener('mouseup', (event) => {
mouseButtons.set(event.button, false);
});
const keyboardKeys = new Map();
const globalKeybindings = new Map();
/**
* @description Registers a keybinding
* @param {string} keyCombo The key combination to bind to
* @param {(event: KeyboardEvent) => void} func The function to call when the key combination is pressed
*/
this.registerKeybinding = (keyCombo, func) => {
keyCombo = keyCombo.split('+').sort().join('+').toLowerCase();
if (!globalKeybindings.has(keyCombo)) globalKeybindings.set(keyCombo, []);
globalKeybindings.get(keyCombo).push(func);
}
document.addEventListener('keydown', (event) => {
keyboardKeys.set(event.key.toLowerCase(), true);
const keyCombo = [...keyboardKeys.keys()].sort().join('+').toLowerCase();
const funcs = globalKeybindings.get(keyCombo) || [];
for (const key of Object.keys(config)) {
if (config[key].type === 'hotkey' && config[key].value.toLowerCase() === keyCombo) {
funcs.push(config[key].action);
}
}
if (funcs?.length) funcs.forEach(f => f(event));
});
/**
* @description Deregisters a keybinding
* @param {string} keyCombo The key combination to deregister
* @param {Function} func The function to deregister
*/
this.deregisterKeybinding = (keyCombo, func) => {
keyCombo = keyCombo.split('+').sort().join('+').toLowerCase();
if (!globalKeybindings.has(keyCombo)) return;
const funcs = globalKeybindings.get(keyCombo);
if (funcs.length === 1) globalKeybindings.delete(keyCombo);
else {
const index = funcs.indexOf(func);
if (index === -1) return;
funcs.splice(index, 1);
}
}
document.addEventListener('keyup', (event) => {
keyboardKeys.delete(event.key.toLowerCase());
});
window.addEventListener('blur', () => {
keyboardKeys.clear();
});
const settings = {
persistenceEnabled: false,
}
const persistentStateStorageKey = 'vasara-storedpersistentstate';
const savePersistentState = () => {
const state = [];
pruneModals();
for (let modal of modals) {
state.push({
title: modal.options.title,
content: modal.content.outerHTML,
width: parseFloat(modal.style.width),
height: parseFloat(modal.style.height),
top: parseFloat(modal.style.top),
left: parseFloat(modal.style.left),
resizable: modal.options.resizable,
disableTitleStacking: modal.options.disableTitleStacking,
enableGhostButton: modal.options.enableGhostButton,
enableCloseButton: modal.options.enableCloseButton,
unique: modal.options.unique,
tag: modal.options.tag,
id: modal.options.id,
isVasaraConfig: modal.isVasaraConfig,
});
}
localStorage.setItem(persistentStateStorageKey, JSON.stringify(state));
this.saveConfig();
}
/**
* @description Call this on load to enable the behavior of trying to persist the state of modal windows between page loads
*/
this.loadPersistentState = () => {
settings.persistenceEnabled = true;
const state = JSON.parse(localStorage.getItem(persistentStateStorageKey));
this.loadConfig();
if (!state);
for (const options of state) {
if (options.isVasaraConfig) {
this.generateConfigWindow(options);
} else {
this.generateModalWindow(options);
}
}
}
window.addEventListener('beforeunload', e => {
savePersistentState();
});
const config = {};
const configLocalStorageKey = 'vasara-storedconfig';
const validateConfigValue = (type, value) => {
switch (type) {
case 'checkbox':
return typeof value === 'boolean';
case 'color':
return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(value);
case 'hotkey':
return /^(?:Ctrl|Alt|Shift|Meta|\b[a-zA-Z]\b)(?:\+(?:Ctrl|Alt|Shift|Meta|\b[a-zA-Z]\b))*$/.test(value);
case 'number':
return !isNaN(value);
default:
return true;
}
}
/**
* @description Query the value of a config key
* @param {string} key
* @returns {any} The stored value
*/
this.queryConfigKey = (key) => {
if (!config[key]) return console.warn('Key does not exist!');
return config[key].value;
}
/**
* @description Set a value
* @param {string} key
* @param {string} value
* @param {boolean} triggerCallback
* @param {boolean} triggerSave
* @returns
*/
this.setConfigValue = (key, value, triggerCallback = true, triggerSave = false) => {
if (!config[key]) return console.warn(`Tried to set the value of unregistered key ${key}`);
if (!validateConfigValue(config[key].type, value)) return console.error(`Tried to give ${config[key].type} '${key}' an invalid value of: `, value);
config[key].value = value;
if (triggerCallback && typeof config[key].callback === 'function') {
config[key].callback(value);
}
if (triggerSave) {
this.saveConfig();
}
}
const serializeConfig = (pretty = false) => {
const object = {};
for (const key of Object.keys(config)) {
object[key] = String(config[key].value);
}
return JSON.stringify(object, null, pretty ? 4 : null);
}
/**
* @description Save the config to localStorage
*/
this.saveConfig = () => {
window.localStorage.setItem(configLocalStorageKey, serializeConfig());
}
const deserializeConfig = (str, triggerCallbacks = false) => {
const object = JSON.parse(str);
if (!object) return console.warn(`No object`);
for (const key of Object.keys(object)) {
if (typeof config[key] !== "object") {
console.warn(`Tried to restore config key '${key}' which has not been registered.`);
continue;
}
const value = object[key];
switch (config[key].type) {
case 'checkbox':
if (value === 'false') this.setConfigValue(key, false, triggerCallbacks);
else if (value === 'true') this.setConfigValue(key, true, triggerCallbacks);
else console.warn(`Tried to give checkbox '${key}' an invalid value of: `, value);
break;
case 'color':
if (validateConfigValue('color', value)) this.setConfigValue(key, value, triggerCallbacks);
else console.warn(`Tried to give color '${key}' an invalid value of: `, value);
break;
case 'hotkey':
if (validateConfigValue('hotkey', value)) this.setConfigValue(key, value, triggerCallbacks);
else console.warn(`Tried to give hotkey '${key}' an invalid value of: `, value);
break;
case 'number':
if (!isNaN(value)) this.setConfigValue(key, parseFloat(value), triggerCallbacks);
else console.warn(`Tried to give number '${key}' a NaN value of: `, value);
break;
case 'dropdown':
case 'text':
case 'hidden':
this.setConfigValue(key, value, triggerCallbacks);
break;
}
}
}
/**
* @description Load the configuration localStorage
*/
this.loadConfig = () => {
deserializeConfig(window.localStorage.getItem(configLocalStorageKey));
updateConfigElements();
}
this.registerConfigValue = ({
key,
value,
display = 'Config Value',
description = 'A configuration value',
type, // 'checkbox', 'color', 'hotkey', 'dropdown', 'number', 'text', 'hidden'
callback = null,
showOnlyIf = null,
action = null, // 'hotkey' only
options = [], // 'dropdown' only
min = null, // 'number' only
max = null, // 'number' only
step = null, // 'number' only
} = {}) => {
if (config[key]) return console.error(`Tried to register an existing key!`);
const configValue = { key, display, description };
if (!['checkbox', 'color', 'hotkey', 'dropdown', 'number', 'text', 'hidden'].includes(type)) return console.error(`Invalid type: ${type}`);
configValue.type = type;
if (!validateConfigValue(type, value)) return console.error(`Tried to give ${type} '${key}' an invalid value of: `, value);
configValue.value = value;
if (type === 'hotkey') {
if (action === null) {
return console.error('Must define an action for a hotkey')
}
Object.assign(configValue, { action });
}
if (type === 'number') {
if (min === null || max === null || step === null) {
return console.error('min, max, step must be defined when registering a number');
}
Object.assign(configValue, { min, max, step });
}
if (type === 'dropdown') {
const opts = [value];
opts.push(...options);
Object.assign(configValue, { options: opts });
}
if (showOnlyIf !== null && typeof showOnlyIf !== 'function') return console.error('Value of showOnlyIf is not a function');
Object.assign(configValue, { showOnlyIf });
if (callback !== null && typeof callback !== 'function') return console.error('Value of onchange is not a functions');
Object.assign(configValue, { callback });
config[key] = configValue;
}
/**
* @description Shorthand function for creating an element and appending it to a parent
* @param {string} type The type of element to create
* @param {string} className The class name(s) to add to the element
* @param {string} alt The alt text for the element
* @param {HTMLElement} parent The parent element to append the new element to
* @returns {HTMLElement} The newly created element
*/
this.createElem = (type, className = '', title, parent) => {
const elem = document.createElement(type);
elem.className = className.split(' ').map(hcn).join(' ');
elem.title = title;
parent?.appendChild(elem);
return elem;
}
const titleDuplicateCounts = {};
let modals = [];
const pruneModals = () => {
modals = modals.filter(e => e.isConnected);
}
/**
* @description Generates a modal window
* @param {Object} options The options for the modal window
* @returns {HTMLElement} The modal window element
*/
this.generateModalWindow = ({
title = 'Modal Window',
content = '',
width = 400,
height = 300,
top = null,
left = null,
resizable = false,
disableTitleStacking = false,
enableGhostButton = true,
enableCloseButton = true,
unique = false,
tag = '',
id = '',
} = {}) => {
if (!disableTitleStacking || unique) {
const titleElements = document.querySelectorAll('.' + hcn('modal-window-header-title'));
let matched = 0;
for (const elem of titleElements) {
if (elem.getAttribute('originalTitle') === title) {
matched++;
}
}
if (matched <= 0) {
titleDuplicateCounts[title] = 0;
}
titleDuplicateCounts[title]++;
if (titleDuplicateCounts[title] > 1) {
if (unique) {
return console.info(`Blocked duplicate window with title: `, title);
}
if (!disableTitleStacking) {
title += ` (${titleDuplicateCounts[title]})`;
}
}
}
const modal = this.createElem('div', 'modal-window', '', document.body);
modal.id = id;
modal.options = { title, content, width, height, top, left, resizable, disableTitleStacking, enableGhostButton, enableCloseButton, unique, tag, id };
modals.push(modal);
const header = this.createElem('div', 'modal-window-header', '', modal);
const titleElem = this.createElem('div', 'modal-window-header-title', '', header);
const buttons = this.createElem('div', 'modal-window-header-buttons', '', header);
const bringToFront = () => {
pruneModals();
modals.forEach(m => m.style.zIndex = 0);
modal.style.zIndex = 1;
}
modal.addEventListener('click', bringToFront);
let x1 = 0, y1 = 0, x2 = 0, y2 = 0, dragging = false;
header.addEventListener('mousedown', e => {
[x2, y2, dragging] = [e.clientX, e.clientY, true];
bringToFront();
});
document.addEventListener('mouseup', e => {
dragging = e.button !== 0 ? dragging : false;
});
document.addEventListener('mousemove', e => {
if (dragging) {
[x1, y1] = [x2 - e.clientX, y2 - e.clientY];
[x2, y2] = [e.clientX, e.clientY];
modal.style.top = `${modal.offsetTop - y1}px`;
modal.style.left = `${modal.offsetLeft - x1}px`;
}
});
titleElem.innerText = title;
titleElem.setAttribute('originalTitle', title);
if (enableGhostButton) createElem('div', 'modal-window-header-button ghost-button', 'Toggle window shimmer', buttons).addEventListener('click', () => modal.classList.toggle(hcn('ghosted')));
if (enableCloseButton) createElem('div', 'modal-window-header-button close-button', 'Close window', buttons).addEventListener('click', e => {
e.stopPropagation();
modal.remove()
});
const contentElem = this.createElem('div', 'modal-window-content', '', modal);
if (content) contentElem.outerHTML = content;
modal.content = contentElem;
Object.assign(modal.style, { width: `${width}px`, height: `${height}px` });
if (top !== null) Object.assign(modal.style, { top: `${top}px` });
if (left !== null) Object.assign(modal.style, { left: `${left}px` });
if (tag) modal.setAttribute('tag', tag)
modal.generateLabel = function ({
text = '',
tooltip = '',
htmlfor = '',
tag = '',
} = {}) {
const label = document.createElement('label');
label.textContent = text;
label.title = tooltip;
label.htmlFor = htmlfor;
label.setAttribute('tag', tag);
contentElem.appendChild(label);
return modal;
}
modal.generateNumberInput = function ({
id = '',
value = 0,
step = null,
max = null,
min = null,
callback = null,
tag = '',
} = {}) {
const input = document.createElement('input');
input.type = 'number';
input.id = id;
input.value = value;
if (step !== null) input.step = step;
if (max !== null) input.max = max;
if (min !== null) input.min = min;
input.setAttribute('tag', tag);
input.addEventListener('change', callback);
contentElem.appendChild(input);
return modal;
}
modal.generateDropdownInput = function ({
id = '',
value = '',
options = [],
callback = null,
tag = '',
} = {}) {
const select = document.createElement('select');
select.id = id;
if (options.indexOf(value) < 0) options.push(value);
for (const opt of options) {
const option = document.createElement('option');
option.value = opt;
option.innerText = opt;
select.appendChild(option);
}
select.value = value;
select.setAttribute('tag', tag);
select.addEventListener('change', callback);
contentElem.appendChild(select);
return modal;
}
modal.generateColorInput = function ({
id = '',
value = '',
callback = null,
tag = '',
} = {}) {
const input = document.createElement('input');
input.type = 'color';
input.id = id;
input.value = value;
input.setAttribute('tag', tag);
input.addEventListener('change', callback);
contentElem.appendChild(input);
return modal;
}
modal.generateCheckboxInput = function ({
id = '',
value = false,
callback = null,
tag = '',
} = {}) {
const input = document.createElement('input');
input.type = 'checkbox';
input.id = id;
input.checked = value;
input.setAttribute('tag', tag);
input.addEventListener('change', callback);
contentElem.appendChild(input);
return modal;
}
modal.generateHotkeyInput = function ({
id = '',
value = '',
callback = null,
tag = '',
} = {}) {
const input = document.createElement('input');
input.id = id;
input.type = 'text';
input.value = value;
input.readOnly = true;
input.addEventListener('focus', () => {
const onKeydown = (e) => {
if (['Control', 'Shift', 'Alt'].includes(e.key)) return;
e.preventDefault();
const combo = (e.ctrlKey ? 'Ctrl+' : '') + (e.shiftKey ? 'Shift+' : '') + (e.altKey ? 'Alt+' : '') + e.key.toUpperCase();
input.value = combo;
callback(combo);
input.blur();
document.removeEventListener('keydown', onKeydown);
};
document.addEventListener('keydown', onKeydown);
});
input.setAttribute('tag', tag);
contentElem.appendChild(input);
return modal;
}
modal.generateStringInput = function ({
id = '',
value = '',
callback = null,
tag = '',
} = {}) {
const input = document.createElement('input');
input.type = 'text';
input.id = id;
input.value = value;
input.setAttribute('tag', tag);
input.addEventListener('change', callback);
contentElem.appendChild(input);
return modal;
}
modal.generateButton = function ({
id = '',
text = '',
callback = null,
tag = '',
} = {}) {
const button = document.createElement('button');
button.id = id;
button.textContent = text;
button.setAttribute('tag', tag);
button.addEventListener('click', callback);
contentElem.appendChild(button);
return modal;
}
modal.putNewline = function () {
const newline = document.createElement('br');
contentElem.appendChild(newline);
return modal;
}
modal.appendElement = function (element) {
contentElem.appendChild(element);
return modal;
}
return modal;
}
const updateConfigElements = () => {
const configElements = document.querySelectorAll('[tag=vasara-config-element]');
for (const elem of configElements) {
switch (config[elem.id].type) {
case 'checkbox':
elem.checked = config[elem.id].value;
break;
case 'hotkey':
case 'color':
case 'number':
case 'dropdown':
case 'text':
elem.value = config[elem.id].value;
break;
}
if (typeof config[elem.id].showOnlyIf === 'function') {
const label = document.querySelector(`[for=${elem.id}]`);
const next = elem.nextElementSibling;
if (config[elem.id].showOnlyIf()) {
elem.style.display = 'inline-block';
label.style.display = 'inline-block';
if (next.tagName === 'BR') next.style.display = 'inline-block';
}
else {
elem.style.display = 'none';
label.style.display = 'none';
if (next.tagName === 'BR') next.style.display = 'none';
}
}
}
}
/**
* @description Shorthand to generate a modal with the contents
* @param {Object} options The options for the modal window
* @returns {HTMLElement} The modal window element
*/
this.generateConfigWindow = ({
title = 'Config Window',
width = 400,
height = 300,
top = null,
left = null,
resizable = false,
disableTitleStacking = false,
enableGhostButton = true,
enableCloseButton = true,
tag = '',
id = '',
} = {}) => {
const modal = this.generateModalWindow({ title, width, height, top, left, resizable, disableTitleStacking, enableGhostButton, enableCloseButton, tag, id, unique: true });
if (!modal) return;
for (const key of Object.keys(config)) {
modal.generateLabel({
text: config[key].display,
tooltip: config[key].description,
htmlfor: key,
});
switch (config[key].type) {
case 'checkbox':
modal.generateCheckboxInput({
value: config[key].value,
callback: e => this.setConfigValue(key, e.target.checked),
tag: 'vasara-config-element',
id: key,
});
break;
case 'color':
modal.generateColorInput({
value: config[key].value,
callback: e => this.setConfigValue(key, e.target.value),
tag: 'vasara-config-element',
id: key,
});
break;
case 'hotkey':
modal.generateHotkeyInput({
value: config[key].value,
callback: v => this.setConfigValue(key, v),
tag: 'vasara-config-element',
id: key,
});
break;
case 'number':
modal.generateNumberInput({
value: config[key].value,
step: config[key].step,
min: config[key].min,
max: config[key].max,
callback: e => this.setConfigValue(key, e.target.value),
tag: 'vasara-config-element',
id: key,
});
break;
case 'dropdown':
modal.generateDropdownInput({
value: config[key].value,
options: config[key].options,
callback: e => this.setConfigValue(key, e.target.value),
tag: 'vasara-config-element',
id: key,
});
break;
case 'text':
modal.generateStringInput({
value: config[key].value,
callback: e => this.setConfigValue(key, e.target.value),
tag: 'vasara-config-element',
id: key,
});
break;
}
modal.putNewline();
}
const configElements = document.querySelectorAll('[tag=vasara-config-element]');
configElements.forEach(e => e.addEventListener('change', updateConfigElements));
updateConfigElements();
modal.isVasaraConfig = true;
return modal;
}
const hcn = className => 'vasara-' + className.split('').reduce((hash, char) => ((hash << 5) - hash + char.charCodeAt(0)) | 0, 0).toString(36);
const injectCss = (css) => {
const hashedCSS = css.replace(/\.([a-zA-Z][a-zA-Z0-9_-]*)/g, (_, className) => `.${hcn(className)}`);
const minifiedCSS = css.replace(/\n/g, '');
const styleSheetElem = document.createElement('style');
if (styleSheetElem.styleSheet) {
styleSheetElem.styleSheet.cssText = minifiedCSS;
} else {
styleSheetElem.appendChild(document.createTextNode(hashedCSS));
}
document.head.appendChild(styleSheetElem);
}
const css = `
.modal-window button,
.modal-window input {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
.modal-window {
position : fixed;
top : 50%;
left : 50%;
transform : translate(-50%, -50%);
border : 1px solid #ccc;
background-color: #fff;
z-index : 1000;
box-shadow : 0 2px 10px rgba(0, 0, 0, 0.1);
font-family : system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
overflow : hidden;
}
.modal-window-header {
display : flex;
justify-content : space-between;
align-items : center;
background-color: #f5f5f5;
padding : 10px;
cursor : move;
}
.modal-window-header-title {
flex: 1;
}
.modal-window-header-buttons {
display: flex;
}
.modal-window-header-button {
width : 15px;
height : 15px;
border-radius: 50%;
margin-left : 5px;
cursor : pointer;
}
.ghost-button {
background-color: #ffcc00;
}
.close-button {
background-color: #ff4d4d;
}
.modal-window-content {
padding : 20px;
overflow-y: scroll;
overflow-x: hidden;
height : 100%;
}
.modal-window-content button {
border : none;
border-radius : 15px;
background-color: #777;
cursor : pointer;
}
.modal-window-content button:hover {
background-color: #aaa;
}
.modal-window-content button:active {
background-color: #ccc;
}
.modal-window-content input {
padding : 8px, 1px;
border : 1px solid #ccc;
border-radius: 4px;
}
.resizable {
resize: both;
}
.ghosted {
opacity: 0.4;
}`;
injectCss(css);
return this;
};
(() => {
const vs = vasara();
const createExploitWindow = () => {
const exploitWindow = vs.generateModalWindow({
title: 'Exploits',
unique: true,
});
if (!exploitWindow) return;
exploitWindow.generateLabel({
text: 'Force Scholars Mate against bot: ',
tooltip: 'This feature simply does not work online. It will only work on the computer play page, and can be used to three crown all bots.'
});
exploitWindow.generateButton({
text: 'Force Scholars Mate',
callback: e => {
e.preventDefault();
if (!document.location.pathname.startsWith('/play/computer')) return alert('You must be on the computer play page to use this feature.');
const board = document.querySelector('wc-chess-board');
if (!board?.game?.move || !board?.game?.getFEN) return alert('You must be in a game to use this feature.');
if (parseInt(board.game.getFEN().split(' ')[5]) > 1 || board.game.getFEN().split(' ')[1] !== 'w') return alert('It must be turn 1 and white to move to use this feature.');
board.game.move('e4');
board.game.move('e5');
board.game.move('Qf3');
board.game.move('Nc6');
board.game.move('Bc4');
board.game.move('Nb8');
board.game.move('Qxf7#');
}
});
exploitWindow.putNewline();
exploitWindow.generateLabel({
text: 'Force Draw against bot: ',
tooltip: 'This feature simply does not work online. It will only work on the computer play page.'
});
exploitWindow.generateButton({
text: 'Force Draw',
callback: e => {
e.preventDefault();
if (document.location.hostname !== 'www.chess.com') return alert('You must be on chess.com to use this feature.');
if (!document.location.pathname.startsWith('/play/computer')) return alert('You must be on the computer play page to use this feature.');
const board = document.querySelector('wc-chess-board');
if (!board?.game?.move) return alert('You must be in a game to use this feature.');
board.game.agreeDraw();
}
});
}
const createConfigWindow = () => {
vs.generateConfigWindow({
height: 700,
resizable: true
});
}
const consoleQueue = [];
const createConsoleWindow = () => {
const consoleWindow = vs.generateModalWindow({
title: 'Console',
resizable: true,
unique: true,
tag: namespace + '_consolewindowtag'
});
if (!consoleWindow) return;
consoleWindow.content.setAttribute('tag', namespace + '_consolewindowcontent');
consoleWindow.content.style.padding = 0;
while (consoleQueue.length > 0) {
addConsoleLineElement(consoleQueue.shift());
}
}
const addConsoleLineElement = (text) => {
const consoleWindow = document.querySelector(`[tag=${namespace}_consolewindowtag]`);
const consoleContent = consoleWindow?.querySelector(`[tag=${namespace}_consolewindowcontent]`);
if (!consoleWindow || !consoleContent) {
return console.warn('Cannot add console line');
}
const line = document.createElement('p');
line.style.border = 'solid 1px';
line.style.width = '100%';
line.style.padding = '2px';
line.innerText = text;
consoleContent.appendChild(line);
}
const addToConsole = (text) => {
const consoleWindow = document.querySelector(`[tag=${namespace}_consolewindowtag]`);
const consoleContent = consoleWindow?.querySelector(`[tag=${namespace}_consolewindowcontent]`);
if (!consoleWindow || !consoleContent) {
consoleQueue.push(text);
return;
}
addConsoleLineElement(text);
}
const namespace = 'chesshook';
window[namespace] = {};
const externalEngineWorkerFunc = () => {
const minIntermediaryVersion = 1;
self.uciQueue = [];
self.hasLock = false;
self.wsPath = null;
self.whatEngine = null;
self.intermediaryVersionString = null;
self.ws = null;
self.enginePassKey = null;
self.closeWs = () => {
if (self.ws !== null) {
self.ws.close();
self.ws = null;
}
};
self.openWs = (url) => {
self.closeWs();
self.ws = new WebSocket(url);
self.ws.onopen = () => {
self.postMessage({ type: 'DEBUG', payload: 'Connected to engine intermediary' });
self.send('whoareyou');
};
self.ws.onclose = () => {
self.postMessage({ type: 'DEBUG', payload: 'Disconnected from engine' });
self.postMessage({ type: 'WSCLOSE' });
self.intermediaryVersionString = null;
};
self.ws.onerror = (e) => {
self.postMessage({ type: 'ERROR', payload: 'Error with engine: ', err: e });
};
self.ws.onmessage = (e) => {
const data = e.data;
if (data.startsWith('iam ')) {
response = data.substring(4);
self.intermediaryVersionString = response;
self.postMessage({ type: 'MESSAGE', payload: 'Connected to engine intermediary version ' + response });
let parts = response.split('v');
if (!parts[1] || parseInt(parts[1]) < minIntermediaryVersion) {
self.postMessage({ type: 'ERROR', payload: 'Engine intermediary version is too old or did not provide a valid version string. Please update it.' });
self.closeWs();
}
self.send('whatengine');
} else if (data.startsWith('auth')) {
if (data === 'authok') {
self.postMessage({ type: 'MESSAGE', payload: 'Engine authentication successful' });
} else {
if (!self.enginePassKey) {
self.postMessage({ type: 'NEEDAUTH' });
} else {
self.postMessage({ type: 'ERROR', payload: 'Engine authentication failed' });
}
}
} else if (data.startsWith('sub')) {
if (data === 'subok') {
} else {
self.postMessage({ type: 'ERROR', payload: 'Engine subscription failed' });
}
} else if (data.startsWith('unsub')) {
if (data === 'unsubok') {
} else {
self.postMessage({ type: 'ERROR', payload: 'Engine unsubscription failed' });
}
} else if (data.startsWith('lock')) {
if (data === 'lockok') {
self.hasLock = true;
while (self.uciQueue.length > 0) {
self.send(self.uciQueue.shift());
}
} else {
self.postMessage({ type: 'ERROR', payload: 'Engine lock failed' });
}
} else if (data.startsWith('unlock')) {
if (data === 'unlockok') {
self.hasLock = false;
} else {
self.postMessage({ type: 'ERROR', payload: 'Engine unlock failed' });
}
} else if (data.startsWith('engine')) {
self.whichEngine = data.split(' ')[1];
self.postMessage({ type: 'ENGINE', payload: self.whichEngine });
} else if (data.startsWith('bestmove')) {
const bestMove = data.split(' ')[1];
self.postMessage({ type: 'BESTMOVE', payload: bestMove });
self.send('unsub');
self.send('unlock');
} else {
self.postMessage({ type: 'UCI', payload: data });
}
};
};
self.send = (data) => {
if (self.ws === null) return self.postMessage({ type: 'ERROR', payload: 'No connection to engine', err: null });
self.ws.send(data);
};
self.addEventListener('message', e => {
if (e.data.type === 'UCI') {
if (!e.data.payload) return self.postMessage({ type: 'ERROR', payload: 'No UCI command provided' });
if (!self.ws) return self.postMessage({ type: 'ERROR', payload: 'No connection to engine' });
if (self.hasLock) {
self.send(e.data.payload);
} else {
self.uciQueue.push(e.data.payload);
}
} else if (e.data.type === 'INIT') {
if (!e.data.payload) return self.postMessage({ type: 'ERROR', payload: 'No URL provided' });
if (!e.data.payload.startsWith('ws://')) return self.postMessage({ type: 'ERROR', payload: 'URL must start with ws://' });
self.openWs(e.data.payload);
self.wsPath = e.data.payload;
} else if (e.data.type === 'AUTH') {
if (!e.data.payload) return self.postMessage({ type: 'ERROR', payload: 'No auth provided' });
self.enginePassKey = e.data.payload;
self.send('auth ' + e.data.payload);
} else if (e.data.type === 'SUB') {
self.send('sub');
} else if (e.data.type === 'UNSUB') {
self.send('unsub');
} else if (e.data.type === 'LOCK') {
if (self.hasLock) return self.postMessage({ type: 'ERROR', payload: 'Already have lock' });
self.send('lock');
} else if (e.data.type === 'UNLOCK') {
self.send('unlock');
} else if (e.data.type === 'WHATENGINE') {
self.send('whatengine');
} else if (e.data.type === 'GETMOVE') {
if (!e.data.payload?.fen) return self.postMessage({ type: 'ERROR', payload: 'No FEN provided' });
if (!e.data.payload?.go) return self.postMessage({ type: 'ERROR', payload: 'No go command provided' });
self.send('lock');
self.send('sub');
self.send('position fen ' + e.data.payload.fen);
self.send(e.data.payload.go);
} else if (e.data.type === 'STOP') {
if (self.hasLock) {
self.send('stop');
self.send('unsub');
self.send('unlock');
}
}
});
}
const externalEngineWorkerBlob = new Blob([`(${externalEngineWorkerFunc.toString()})();`], { type: 'application/javascript' });
const externalEngineWorkerURL = URL.createObjectURL(externalEngineWorkerBlob);
const externalEngineWorker = new Worker(externalEngineWorkerURL);
let externalEngineName = null;
externalEngineWorker.onmessage = (e) => {
const maxlines = 50;
const websocketOutputTextArea = document.getElementById(namespace + '_websocketoutput');
const engineOutputTextArea = document.getElementById(namespace + '_engineoutput');
const addToWebSocketOutput = (line) => {
if (websocketOutputTextArea) {
const lines = websocketOutputTextArea.value.split('\n');
lines.push(line);
if (lines.length > maxlines) {
lines.shift();
}
websocketOutputTextArea.value = lines.join('\n');
}
}
const updateEngineTextarea = (infoLine) => {
if (engineOutputTextArea) {
const lines = engineOutputTextArea.value.split('\n');
const infoParts = infoLine.split(' ');
const depth = infoParts[infoParts.indexOf('depth') + 1];
const score = infoParts[infoParts.indexOf('score') + 1] + ' ' + infoParts[infoParts.indexOf('score') + 2];
const time = infoParts[infoParts.indexOf('time') + 1];
const bestLine = infoParts.slice(infoParts.indexOf('pv') + 1).join(' ');
if (depth !== 'info') {
lines[0] = 'depth ' + depth;
}
if (!score.startsWith('info')) {
lines[1] = 'score ' + score;
}
if (time !== 'info') {
lines[2] = 'time ' + time;
}
if (!bestLine.startsWith('info')) {
lines[3] = 'best line ' + bestLine;
}
engineOutputTextArea.value = lines.join('\n');
}
}
if (e.data.type === 'DEBUG') {
console.debug(e.data.payload);
addToWebSocketOutput(e.data.payload);
} else if (e.data.type === 'ERROR') {
console.error(e.data.payload, e.data.err);
addToWebSocketOutput(e.data.payload);
} else if (e.data.type === 'MESSAGE') {
addToConsole(e.data.payload);
addToWebSocketOutput(e.data.payload);
} else if (e.data.type === 'UCI') {
updateEngineTextarea(e.data.payload);
} else if (e.data.type === 'ENGINE') {
externalEngineName = e.data.payload;
addToWebSocketOutput('Connected to ' + externalEngineName);
} else if (e.data.type === 'NEEDAUTH') {
externalEngineWorker.postMessage({ type: 'AUTH', payload: vs.queryConfigKey(namespace + '_externalenginepasskey') });
addToWebSocketOutput('Attempting to authenticate with passkey ' + vs.queryConfigKey(namespace + '_externalenginepasskey'));
} else if (e.data.type === 'BESTMOVE') {
addToConsole(`${externalEngineName} engine computed best move: ${e.data.payload}`);
handleEngineMove(e.data.payload);
}
}
const betafishWebWorkerFunc = () => {
self.instance = betafishEngine();
self.thinking = false;
const postError = (message) => self.postMessage({ type: 'ERROR', payload: message });
const isInstanceInitialized = () => self.instance || postError('Betafish not initialized.');
self.addEventListener('message', e => {
if (!isInstanceInitialized()) return;
switch (e.data.type) {
case 'FEN':
if (!e.data.payload) return postError('No FEN provided.');
self.instance.setFEN(e.data.payload);
break;
case 'GETMOVE':
if (self.thinking) return postError('Betafish is already calculating.');
self.postMessage({ type: 'MESSAGE', payload: 'Betafish received request for best move. Calculating...' });
self.thinking = true;
const move = self.instance.getBestMove();
self.thinking = false;
self.postMessage({ type: 'MOVE', payload: { move, toMove: self.instance.getFEN().split(' ')[1] } });
break;
case 'THINKINGTIME':
if (isNaN(e.data.payload)) return postError('Invalid thinking time provided.');
self.instance.setThinkingTime(e.data.payload / 1000);
self.postMessage({ type: 'DEBUG', payload: `Betafish thinking time set to ${e.data.payload}ms.` });
break;
default:
postError('Invalid message type.');
}
});
};
const betafishWorkerBlob = new Blob([`const betafishEngine=${betafishEngine.toString()};(${betafishWebWorkerFunc.toString()})();`], { type: 'application/javascript' });
const betafishWorkerURL = URL.createObjectURL(betafishWorkerBlob);
const betafishWorker = new Worker(betafishWorkerURL);
const betafishPieces = { EMPTY: 0, wP: 1, wN: 2, wB: 3, wR: 4, wQ: 5, wK: 6, bP: 7, bN: 8, bB: 9, bR: 10, bQ: 11, bK: 12 };
betafishWorker.onmessage = e => {
switch (e.data.type) {
case 'DEBUG':
case 'MESSAGE':
console.info(e.data.payload);
break;
case 'ERROR':
console.error(e.data.payload);
break;
case 'MOVE':
const { move, toMove } = e.data.payload;
const squareToRankFile = sq => [Math.floor((sq - 21) / 10), sq - 21 - Math.floor((sq - 21) / 10) * 10];
const from = squareToRankFile(move & 0x7f);
const to = squareToRankFile((move >> 7) & 0x7f);
const promoted = (move >> 20) & 0xf;
const promotedString = promoted !== 0 ? Object.entries(betafishPieces).find(([key, value]) => value === promoted)?.[0].toLowerCase()[1] || '' : '';
const uciMove = coordsToUCIMoveString(from, to, promotedString);
addToConsole(`Betafish computed best for ${toMove === 'w' ? 'white' : 'black'}: ${uciMove}`);
handleEngineMove(uciMove);
break;
}
};
const originalFetch = window.fetch;
window.fetch = async (...args) => {
const response = await originalFetch(...args);
const clonedResponse = response.clone();
clonedResponse.json().then(body => {
try {
handleInterception({ url: args[0].url || args[0] }, body);
} catch (ignored) {
}
}).catch(error => console.error('Fetch response clone error:', error));
return response;
};
const originalXHROpen = XMLHttpRequest.prototype.open;
const originalXHRSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url) {
this._url = new URL(url, window.location.origin).href;
originalXHROpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function (body) {
this.addEventListener('load', () => {
if (this.readyState === 4 && this.status >= 200 && this.status < 300) {
try {
const responseJson = JSON.parse(this.responseText);
handleInterception({ url: this._url }, responseJson);
} catch (ignored) {
}
}
});
originalXHRSend.apply(this, arguments);
};
const handleInterception = (req, res) => {
const urlPath = new URL(req.url).pathname;
switch (urlPath) {
case '/callback/tactics/rated/next':
if (vs.queryConfigKey(namespace + '_puzzlemode')) {
puzzleQueue.push({
fen: res.initialFen,
moves: decodeTCN(res.tcnMoveList),
tagged: false,
});
}
break;
case '/callback/tactics/challenge/puzzles':
if (vs.queryConfigKey(namespace + '_puzzlemode')) {
for (const puzzle of res.puzzles) {
puzzleQueue.push({
fen: puzzle.initialFen,
moves: decodeTCN(puzzle.tcnMoveList),
tagged: false,
});
}
}
break;
}
}
const init = () => {
vs.registerConfigValue({
key: namespace + '_configwindowhotkey',
type: 'hotkey',
display: 'Config Window Hotkey: ',
description: 'The hotkey to show the conifg window',
value: 'Alt+K',
action: createConfigWindow
});
vs.registerConfigValue({
key: namespace + '_consolewindowhotkey',
type: 'hotkey',
display: 'Console Window Hotkey: ',
description: 'The hotkey to show the console window',
value: 'Alt+C',
action: createConsoleWindow
});
vs.registerConfigValue({
key: namespace + '_exploitwindowhotkey',
type: 'hotkey',
display: 'Exploit Window Hotkey: ',
description: 'The hotkey to show the exploit window',
value: 'Alt+L',
action: createExploitWindow
});
vs.registerConfigValue({
key: namespace + '_renderthreats',
type: 'checkbox',
display: 'Render Threats: ',
description: 'Render mates, undefended pieces, underdefended pieces, and pins.',
value: true
});
vs.registerConfigValue({
key: namespace + '_renderthreatspincolor',
type: 'color',
display: 'Pin Color: ',
description: 'The color to render pins in',
value: '#3333ff',
showOnlyIf: () => vs.queryConfigKey(namespace + '_renderthreats')
});
vs.registerConfigValue({
key: namespace + '_renderthreatsundefendedcolor',
type: 'color',
display: 'Undefended Color: ',
description: 'The color to render undefended pieces in',
value: '#ffff00',
showOnlyIf: () => vs.queryConfigKey(namespace + '_renderthreats')
});
vs.registerConfigValue({
key: namespace + '_renderthreatsunderdefendedcolor',
type: 'color',
display: 'Underdefended Color: ',
description: 'The color to render underdefended pieces in',
value: '#ff6666',
showOnlyIf: () => vs.queryConfigKey(namespace + '_renderthreats')
});
vs.registerConfigValue({
key: namespace + '_renderthreatsmatecolor',
type: 'color',
display: 'Mate Color: ',
description: 'The color to render mates in',
value: '#ff0000',
showOnlyIf: () => vs.queryConfigKey(namespace + '_renderthreats')
});
vs.registerConfigValue({
key: namespace + '_cleararrowskey',
type: 'hotkey',
display: 'Clear Arrows Hotkey: ',
description: 'The hotkey to clear arrows',
value: 'Alt+L',
action: () => {
const board = document.querySelector('wc-chess-board');
if (!board) return;
board.game.markings.removeAll();
}
});
vs.registerConfigValue({
key: namespace + '_autoqueue',
type: 'checkbox',
display: 'Auto Queue: ',
description: 'Attempts to automatically queue for games.',
value: false
});
vs.registerConfigValue({
key: namespace + '_legitmode',
type: 'checkbox',
display: 'Legit Mode: ',
description: 'Prevents the script from doing anything that could be considered cheating.',
value: false,
callback: () => {
vs.setConfigValue('whichEngine', 'none');
vs.setConfigValue('autoMove', false);
vs.setConfigValue('puzzleMode', false);
}
});
vs.registerConfigValue({
key: namespace + '_playingas',
type: 'dropdown',
display: 'Playing As: ',
description: 'What color to calculate moves for',
value: 'both',
options: ['both', 'white', 'black', 'auto'],
showOnlyIf: () => !vs.queryConfigKey(namespace + '_legitmode') && !vs.queryConfigKey(namespace + '_puzzlemode')
});
vs.registerConfigValue({
key: namespace + '_enginemovecolor',
type: 'color',
display: 'Engine Move Color: ',
description: 'The color to render the engine\'s move in',
value: '#77ff77',
showOnlyIf: () => !vs.queryConfigKey(namespace + '_legitmode') && !vs.queryConfigKey(namespace + '_puzzlemode')
});
vs.registerConfigValue({
key: namespace + '_whichengine',
type: 'dropdown',
display: 'Which Engine: ',
description: 'Which engine to use',
value: 'none',
options: ['none', 'betafish', 'random', 'cccp', 'external'],
showOnlyIf: () => !vs.queryConfigKey(namespace + '_legitmode') && !vs.queryConfigKey(namespace + '_puzzlemode'),
callback: () => {
if (vs.queryConfigKey(namespace + '_whichengine') !== 'external') {
return;
}
if (!vs.queryConfigKey(namespace + '_externalengineurl')) {
addToConsole('Please set the path to the external engine in the config.');
return;
}
externalEngineWorker.postMessage({ type: 'INIT', payload: vs.queryConfigKey(namespace + '_externalengineurl') });
if (!vs.queryConfigKey(namespace + '_haswarnedaboutexternalengine') || vs.queryConfigKey(namespace + '_haswarnedaboutexternalengine') === 'false') {
addToConsole('Please note that the external engine is not for the faint of heart. It requires tinkering and the user to host the chesshook intermediary server.');
alert('Please note that the external engine is not for the faint of heart. It requires tinkering and the user to host the chesshook intermediary server.')
vs.setConfigValue(namespace + '_haswarnedaboutexternalengine', true);
}
}
});
vs.registerConfigValue({
key: namespace + '_betafishthinkingtime',
type: 'number',
display: 'Betafish Thinking Time: ',
description: 'The amount of time in ms to think for each move',
value: 1000,
min: 0,
max: 20000,
step: 100,
showOnlyIf: () => !vs.queryConfigKey(namespace + '_legitmode') && vs.queryConfigKey(namespace + '_whichengine') === 'betafish',
callback: () => {
betafishWorker.postMessage({ type: 'THINKINGTIME', payload: parseFloat(vs.queryConfigKey(namespace + '_betafishthinkingtime')) });
}
});
vs.registerConfigValue({
key: namespace + '_externalengineurl',
type: 'text',
display: 'External Engine URL: ',
description: 'The URL of the external engine',
value: 'ws://localhost:8080/ws',
showOnlyIf: () => !vs.queryConfigKey(namespace + '_legitmode') && vs.queryConfigKey(namespace + '_whichengine') === 'external',
callback: v => externalEngineWorker.postMessage({ type: 'INIT', payload: v })
});
vs.registerConfigValue({
key: namespace + '_externalengineautogocommand',
type: 'checkbox',
display: 'External Engine Auto Go Command: ',
description: 'Automatically determine the go command based on the time left in the game',
value: true,
showOnlyIf: () => !vs.queryConfigKey(namespace + '_legitmode') && vs.queryConfigKey(namespace + '_whichengine') === 'external'
});
vs.registerConfigValue({
key: namespace + '_externalenginegocommand',
type: 'text',
display: 'External Engine Go Command: ',
description: 'The command to send to the external engine to start thinking',
value: 'go movetime 1000',
showOnlyIf: () => !vs.queryConfigKey(namespace + '_legitmode') && vs.queryConfigKey(namespace + '_whichengine') === 'external' && !vs.queryConfigKey(namespace + '_externalengineautogocommand')
});
vs.registerConfigValue({
key: namespace + '_externalenginepasskey',
type: 'text',
display: 'External Engine Passkey: ',
description: 'The passkey to send to the external engine to authenticate',
value: 'passkey',
showOnlyIf: () => !vs.queryConfigKey(namespace + '_legitmode') && vs.queryConfigKey(namespace + '_whichengine') === 'external',
callback: v => externalEngineWorker.postMessage({ type: 'AUTH', payload: v })
});
vs.registerConfigValue({
key: namespace + '_automove',
type: 'checkbox',
display: 'Auto Move: ',
description: 'Potentially bannable. Tries to randomize move times to avoid detection.',
value: false,
showOnlyIf: () => !vs.queryConfigKey(namespace + '_legitmode') && !vs.queryConfigKey(namespace + '_puzzlemode')
});
vs.registerConfigValue({
key: namespace + '_automovemaxrandomdelay',
type: 'number',
display: 'Move time target range max: ',
description: 'The maximum delay in ms for automove to target',
value: 1000,
min: 0,
max: 20000,
step: 100,
showOnlyIf: () => !vs.queryConfigKey(namespace + '_legitmode') && vs.queryConfigKey(namespace + '_automove')
});
vs.registerConfigValue({
key: namespace + '_automoveminrandomdelay',
type: 'number',
display: 'Move time target range min: ',
description: 'The minimum delay in ms for automove to target',
value: 500,
min: 0,
max: 20000,
step: 100,
showOnlyIf: () => !vs.queryConfigKey(namespace + '_legitmode') && vs.queryConfigKey(namespace + '_automove')
});
vs.registerConfigValue({
key: namespace + '_automoveinstamovestart',
type: 'checkbox',
display: 'Speed up game start: ',
description: 'Instantly move first 5',
value: true,
showOnlyIf: () => !vs.queryConfigKey(namespace + '_legitmode') && vs.queryConfigKey(namespace + '_automove')
});
vs.registerConfigValue({
key: namespace + '_puzzlemode',
type: 'checkbox',
display: 'Solves puzzles: ',
description: 'Solves puzzles automatically',
value: false,
callback: () => {
vs.setConfigValue('whichEngine', 'none');
vs.setConfigValue('autoMove', false);
}
});
vs.registerConfigValue({
key: namespace + '_refreshhotkey',
type: 'hotkey',
display: 'Refresh Hotkey: ',
description: 'Force some values to reload in order to try to "unstuck" some features',
value: 'Alt+R',
action: () => {
if (window.location.pathname.startsWith('/puzzles')) {
window.location.reload();
} else {
engineLastKnownFEN = null;
}
}
});
vs.registerConfigValue({
key: namespace + '_renderwindow',
type: 'hidden',
value: true
});
vs.registerConfigValue({
key: namespace + '_haswarnedaboutexternalengine',
type: 'hidden',
value: false
});
vs.loadPersistentState();
addToConsole(`Loaded! This is version ${GM_info.script.version}`);
addToConsole(`Github: https://github.com/0mlml/chesshook`);
if (vs.queryConfigKey(namespace + '_externalengineurl') && vs.queryConfigKey(namespace + '_whichengine') === 'external') {
externalEngineWorker.postMessage({ type: 'INIT', payload: vs.queryConfigKey(namespace + '_externalengineurl') });
}
}
const decodeTCN = (n) => {
const tcnChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!?{~}(^)[_]@#$,./&-*++=";
const pieceChars = "qnrbkp";
let moves = [];
for (let i = 0; i < n.length; i += 2) {
let move = {
from: null,
to: null,
drop: null,
promotion: null,
};
let o = tcnChars.indexOf(n[i]);
let s = tcnChars.indexOf(n[i + 1]);
if (s > 63) {
move.promotion = pieceChars[Math.floor((s - 64) / 3)];
s = o + (o < 16 ? -8 : 8) + ((s - 1) % 3) - 1;
}
if (o > 75) {
move.drop = pieceChars[o - 79];
} else {
move.from = tcnChars[o % 8] + String(Math.floor(o / 8) + 1);
}
move.to = tcnChars[s % 8] + String(Math.floor(s / 8) + 1);
moves.push(move);
}
return moves;
}
const getPieceValue = (piece, scoreActivity = false) => {
return {
'p': 1,
'n': 3,
'b': 3,
'r': 5,
'q': 9,
'k': scoreActivity ? -3 : 99
}[piece.toLowerCase()];
}
const xyToCoordInverted = (x, y) => {
const letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
const file = letters[y];
const rank = x + 1;
return file + rank;
}
const coordToYX = (coord) => {
const letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
const file = letters.indexOf(coord[0]) + 1;
const rank = Number(coord[1]);
return [file, rank];
}
const coordsToUCIMoveString = (from, to, promotion) => {
return xyToCoordInverted(from[0], from[1]) + xyToCoordInverted(to[0], to[1]) + promotion;
}
let renderThreatsLastKnownFEN = null;
const renderThreats = () => {
let board = document.querySelector('wc-chess-board');
if (renderThreatsLastKnownFEN === board.game.getFEN()) return;
renderThreatsLastKnownFEN = board.game.getFEN();
board.game.markings.removeAll();
const threats = board.game.getJCEGameCopy().threats();
for (let pin of threats.pins) {
board.game.markings.addOne({ type: 'highlight', data: { color: vs.queryConfigKey(namespace + '_renderthreatspincolor'), square: pin } });
}
for (let undefended of threats.undefended) {
board.game.markings.addOne({ type: 'arrow', data: { color: vs.queryConfigKey(namespace + '_renderthreatsundefendedcolor'), from: undefended.substring(0, 2), to: undefended.substring(2, 4) } });
}
for (let underdefended of threats.underdefended) {
board.game.markings.addOne({ type: 'arrow', data: { color: vs.queryConfigKey(namespace + '_renderthreatsunderdefendedcolor'), from: underdefended.substring(0, 2), to: underdefended.substring(2, 4) } });
}
for (let mate of threats.mates) {
board.game.markings.addOne({ type: 'arrow', data: { color: vs.queryConfigKey(namespace + '_renderthreatsmatecolor'), from: mate.substring(0, 2), to: mate.substring(2, 4) } });
}
}
const resolveAfterMs = (ms = 1000) => {
if (ms <= 0) return new Promise(res => res());
return new Promise(res => setTimeout(res, ms));
}
const mergeMoveToUCI = (move) => move.from + move.to + (move.promotion ? move.promotion : '');
const cccpEngine = () => {
const board = document.querySelector('wc-chess-board');
const legalMoves = board.game.getLegalMoves();
if (legalMoves.length === 0) return;
const checkmates = legalMoves.filter(m => m.san.includes('#'));
if (checkmates.length > 0) {
return mergeMoveToUCI(checkmates[0]);
}
const checks = legalMoves.filter(m => m.san.includes('+'));
const captureMoves = legalMoves.filter(m => m.san.includes('x'));
const goodCaptureExists = captureMoves.some(m => {
const capturedValue = getPieceValue(m.captured, true);
return capturedValue > 4 || getPieceValue(m.piece, true) < capturedValue;
});
if (checks.length > 0 && !goodCaptureExists) {
return mergeMoveToUCI(checks[0]);
}
if (captureMoves.length > 0) {
return mergeMoveToUCI(captureMoves.sort((a, b) => (getPieceValue(b.captured) - getPieceValue(b.piece) + getPieceValue(b.captured) * 0.1) - (getPieceValue(a.captured) - getPieceValue(b.piece) + getPieceValue(a.captured) * 0.1))[0]);
}
const pushes = legalMoves.sort((a, b) => {
let scoreA = getPieceValue(a.piece, true);
let scoreB = getPieceValue(b.piece, true);
const columnScores = { 'a': -1, 'b': 0, 'c': 1, 'd': 3, 'e': 3, 'f': 1, 'g': 0, 'h': -1 };
scoreA += columnScores[a.to[0]];
scoreB += columnScores[b.to[0]];
const scorePush = (to, isWhite) => {
const toRow = parseInt(to[1]);
return isWhite ? toRow : 9 - toRow;
}
scoreA += scorePush(a.to, a.color === 1);
scoreB += scorePush(b.to, b.color === 1);
a.score = scoreA;
b.score = scoreB;
return scoreB - scoreA;
});
return mergeMoveToUCI(pushes[0]);
}
const isMyTurn = () => {
const board = document.querySelector('wc-chess-board');
const fen = board.game.getFEN();
if (vs.queryConfigKey(namespace + '_playingas') !== 'both') {
if ((vs.queryConfigKey(namespace + '_playingas') === 'white' && fen.split(' ')[1] === 'b') ||
(vs.queryConfigKey(namespace + '_playingas') === 'black' && fen.split(' ')[1] === 'w')) {
return false;
}
}
if (vs.queryConfigKey(namespace + '_playingas') === 'auto') {
const playingAs = board.game.getPlayingAs() === 1 ? 'w' : board.game.getPlayingAs() === 2 ? 'b' : null;
return playingAs === null || fen.split(' ')[1] === playingAs;
}
return true;
}
let lastEngineMoveCalcStartTime = performance.now();
let engineLastKnownFEN = null;
const getEngineMove = () => {
const board = document.querySelector('wc-chess-board');
const fen = board.game.getFEN();
if (!fen || engineLastKnownFEN === fen) return;
engineLastKnownFEN = board.game.getFEN();
if (!isMyTurn()) return;
addToConsole(`Calculating move based on engine: ${vs.queryConfigKey(namespace + '_whichengine')}...`);
if (vs.queryConfigKey(namespace + '_automoveinstamovestart') && parseInt(fen.split(' ')[5]) < 6) lastEngineMoveCalcStartTime = 0;
else lastEngineMoveCalcStartTime = performance.now();
if (vs.queryConfigKey(namespace + '_whichengine') === 'betafish') {
betafishWorker.postMessage({ type: 'FEN', payload: fen });
betafishWorker.postMessage({ type: 'GETMOVE' });
} else if (vs.queryConfigKey(namespace + '_whichengine') === 'external') {
if (!externalEngineName) {
addToConsole('External engine appears to be disconnected. Please check the config.');
return;
}
let goCommand = vs.queryConfigKey(namespace + '_externalengineautogocommand');
if (!vs.queryConfigKey(namespace + '_externalengineautogocommand') && (!goCommand || !goCommand.includes('go'))) {
addToConsole('External engine go command is invalid. Please check the config.');
return;
} else if (vs.queryConfigKey(namespace + '_externalengineautogocommand')) {
goCommand = 'go';
if (board?.game?.timeControl && board.game.timeControl.get() && board.game.timestamps.get) {
const increment = board.game.timeControl.get().increment;
const baseTime = board.game.timeControl.get().baseTime;
let whiteTime = baseTime
let blackTime = baseTime;
const timestamps = board.game.timestamps.get();
for (let i in timestamps) {
if (i % 2 === 0) {
whiteTime -= timestamps[i] * 100;
whiteTime += increment;
} else {
blackTime -= timestamps[i] * 100;
blackTime += increment;
}
}
goCommand += ` wtime ${whiteTime} btime ${blackTime} winc ${increment} binc ${increment}`;
} else {
goCommand += ' depth 20';
}
}
addToConsole('External engine is: ' + externalEngineName);
externalEngineWorker.postMessage({ type: 'GETMOVE', payload: { fen: fen, go: goCommand } });
} else if (vs.queryConfigKey(namespace + '_whichengine') === 'random') {
const legalMoves = document.querySelector('wc-chess-board').game.getLegalMoves()
const randomMove = legalMoves[Math.floor(Math.random() * legalMoves.length)];
addToConsole(`Random computed move: ${randomMove.san}`);
handleEngineMove(randomMove.from + randomMove.to + (randomMove.promotion ? randomMove.promotion : ''));
} else if (vs.queryConfigKey(namespace + '_whichengine') === 'cccp') {
const move = cccpEngine();
if (!move) return;
addToConsole(`CCCP computed move: ${move}`);
handleEngineMove(move);
}
}
const calculateDOMSquarePosition = (square, fromDoc = true) => {
const board = document.getElementsByTagName('wc-chess-board')[0];
if (!board?.game) return;
const { left, top, width } = board.getBoundingClientRect();
const squareWidth = width / 8;
const correction = squareWidth / 2;
const coords = coordToYX(square);
if (!board.game.getOptions().flipped) {
return {
x: left + squareWidth * coords[0] - correction,
y: top + width - squareWidth * coords[1] + correction,
};
} else {
return {
x: left + width - squareWidth * coords[0] + correction,
y: top + squareWidth * coords[1] - correction,
};
}
}
let handleMoveLastKnownMarking = null;
const handleEngineMove = (uciMove) => {
const board = document.querySelector('wc-chess-board');
if (!board?.game) return false;
if (!vs.queryConfigKey(namespace + '_renderthreats')) board.game.markings.removeAll();
const marking = { type: 'arrow', data: { color: vs.queryConfigKey(namespace + '_enginemovecolor'), from: uciMove.substring(0, 2), to: uciMove.substring(2, 4) } };
if (handleMoveLastKnownMarking) board.game.markings.removeOne(handleMoveLastKnownMarking);
board.game.markings.addOne(marking);
handleMoveLastKnownMarking = marking;
if (!vs.queryConfigKey(namespace + '_automove')) {
return;
}
let max = vs.queryConfigKey(namespace + '_automovemaxrandomdelay'), min = vs.queryConfigKey(namespace + '_automoveminrandomdelay');
if (min > max) {
min = max;
}
const delay = (Math.floor(Math.random() * (max - min)) + min) - (performance.now() - lastEngineMoveCalcStartTime);
resolveAfterMs(delay).then(() => {
if (['/play/computer', '/analysis'].some(p => document.location.pathname.startsWith(p))) {
board.game.move(uciMove);
} else {
if (uciMove.length > 4) {
board.game.move({
from: uciMove.substring(0, 2),
to: uciMove.substring(2, 4),
promotion: uciMove.substring(4, 5),
animate: false,
userGenerated: true
});
} else {
const fromPos = calculateDOMSquarePosition(uciMove.substring(0, 2));
const toPos = calculateDOMSquarePosition(uciMove.substring(2, 4));
board.dispatchEvent(new PointerEvent('pointerdown', {
bubbles: true,
cancelable: true,
view: window,
clientX: fromPos.x,
clientY: fromPos.y,
}));
board.dispatchEvent(new PointerEvent('pointerup', {
bubbles: true,
cancelable: true,
view: window,
clientX: toPos.x,
clientY: toPos.y,
}));
}
}
});
}
const handlePuzzleMove = (moveObj) => {
const board = document.querySelector('wc-chess-board');
if (!board?.game) return false;
if (moveObj.promotion) {
board.game.move({
from: moveObj.from,
to: moveObj.to,
promotion: moveObj.promotion,
animate: false,
userGenerated: true
});
} else {
const fromPos = calculateDOMSquarePosition(moveObj.from);
const toPos = calculateDOMSquarePosition(moveObj.to);
board.dispatchEvent(new PointerEvent('pointerdown', {
bubbles: true,
cancelable: true,
view: window,
clientX: fromPos.x,
clientY: fromPos.y,
}));
board.dispatchEvent(new PointerEvent('pointerup', {
bubbles: true,
cancelable: true,
view: window,
clientX: toPos.x,
clientY: toPos.y,
}));
}
}
let requeueLastGamePath = null;
let requeueAttempts = 0;
const handleRequeue = () => {
if (requeueLastGamePath === null) {
requeueLastGamePath = window.location.pathname;
requeueAttempts = 0;
} else if (requeueLastGamePath !== window.location.pathname) {
requeueLastGamePath = null;
}
if (requeueLastGamePath === window.location.pathname) {
try {
document.querySelector('div.tabs-tab:nth-child(2)').click();
document.querySelector('.create-game-component > button:nth-child(2)').click();
} catch {
if (requeueAttempts.requeueAttempts > 10) {
requeueLastGamePath = null;
requeueAttempts = 0;
} else {
requeueAttempts.requeueAttempts++;
}
}
}
}
const fuzzyFensEqual = (fen1, fen2) => fen1.split(' ').slice(0, 1).join(' ') === fen2.split(' ').slice(0, 1).join(' ');
const puzzleQueue = [];
let lastPuzzleFEN = null;
let playerTurn = false;
window[namespace].getPuzzleQueue = () => puzzleQueue;
const puzzleHandler = () => {
const board = document.querySelector('wc-chess-board');
if (!board) return;
const currentFEN = board.game.getFEN();
for (let i = 0; i < puzzleQueue.length; i++) {
const puzzle = puzzleQueue[i];
if (fuzzyFensEqual(puzzle.fen, currentFEN)) {
puzzle.tagged = true;
}
if (puzzle.tagged) {
if (lastPuzzleFEN && fuzzyFensEqual(currentFEN, lastPuzzleFEN)) return;
while (puzzle.moves.length > 0 && !playerTurn) {
puzzle.moves.shift();
playerTurn = !playerTurn;
}
if (puzzle.moves.length > 0 && playerTurn) {
const move = puzzle.moves.shift();
handlePuzzleMove(move);
lastPuzzleFEN = currentFEN;
playerTurn = !playerTurn;
return;
} else {
puzzleQueue.splice(i, 1);
lastPuzzleFEN = null;
playerTurn = false;
break;
}
}
}
}
const clickPuzzleNext = () => {
const nextButton = document.querySelector('button.ui_v5-button-component:nth-child(3)');
const arrowIcon = nextButton?.querySelector('.arrow-right');
if (arrowIcon) {
nextButton.click();
}
}
const updateLoop = () => {
const board = document.querySelector('wc-chess-board');
if (!board?.game) return;
if (board.game.getPositionInfo().gameOver) {
externalEngineWorker.postMessage({ type: 'STOP' });
if (vs.queryConfigKey(namespace + '_autoqueue')) {
handleRequeue();
}
}
if (document.location.pathname.startsWith('/puzzles')) {
if (vs.queryConfigKey(namespace + '_puzzlemode')) {
puzzleHandler();
clickPuzzleNext();
}
}
if (vs.queryConfigKey(namespace + '_renderthreats')) {
renderThreats();
}
if (!vs.queryConfigKey(namespace + '_legitmode') && vs.queryConfigKey(namespace + '_whichengine') !== 'none') {
getEngineMove();
}
}
window[namespace].updateLoop = setInterval(updateLoop, 20);
document.addEventListener('readystatechange', () => {
if (document.readyState === 'interactive') {
init();
}
});
})();