Chesshook greasyfork

Chess.com Cheat Userscript

// ==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();
    }
  });
})();