Search code examples
javascriptarrayschess

Developing mailbox chess engine 'GenerateAllMoves' not working as expected


GenerateAllMoves function does not seem to work.

I ran this code but the results were strange.

const BOARD_IDX = 120;

// black pieces
const W_P = 1;
const W_R = 2;
const W_B = 3;
const W_N = 4;
const W_Q = 5;
const W_K = 6;

// 블랙의 피스들
const B_P = -W_P;
const B_R = -W_R;
const B_B = -W_B;
const B_N = -W_N;
const B_Q = -W_Q;
const B_K = -W_K;

const RAND = 100; // padding
const EMPTY = 0; // blank

// remainder constant
const A8 = 21,
  B8 = 22,
  C8 = 23,
  D8 = 24,
  E8 = 25,
  F8 = 26,
  G8 = 27,
  H8 = 28,
  A7 = 31,
  B7 = 32,
  C7 = 33,
  D7 = 34,
  E7 = 35,
  F7 = 36,
  G7 = 37,
  H7 = 38,
  A2 = 81,
  B2 = 82,
  C2 = 83,
  D2 = 84,
  E2 = 85,
  F2 = 86,
  G2 = 87,
  H2 = 88,
  A1 = 91,
  B1 = 92,
  C1 = 93,
  D1 = 94,
  E1 = 95,
  F1 = 96,
  G1 = 97,
  H1 = 98;

const WHITE_PROMOTES = [A8, B8, C8, D8, E8, F8, G8, H8];
const BLACK_PROMOTES = [A1, B1, C1, D1, E1, F1, G1, H1];

const WHITE_NEAR_PROMOTES = [A7, B7, C7, D7, E7, F7, G7, H7];
const BLACK_NEAR_PROMOTES = [A2, B2, C2, D2, E2, F2, G2, H2];

const WHITE_PAWN = [A2, B2, C2, D2, E2, F2, G2, H2];
const BLACK_PAWN = [A7, B7, C7, D7, E7, F7, G7, H7];

// Default location on the board
const InitialPosition = [
  RAND, RAND, RAND, RAND, RAND, RAND, RAND, RAND, RAND, RAND,
  RAND, RAND, RAND, RAND, RAND, RAND, RAND, RAND, RAND, RAND,
  RAND, B_R, B_N, B_B, B_Q, B_K, B_B, B_N, B_R, RAND,
  RAND, B_P, B_P, B_P, B_P, B_P, B_P, B_P, B_P, RAND,
  RAND, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, RAND,
  RAND, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, RAND,
  RAND, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, RAND,
  RAND, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, RAND,
  RAND, W_P, W_P, W_P, W_P, W_P, W_P, W_P, W_P, RAND,
  RAND, W_R, W_N, W_B, W_Q, W_K, W_B, W_N, W_R, RAND,
  RAND, RAND, RAND, RAND, RAND, RAND, RAND, RAND, RAND, RAND,
  RAND, RAND, RAND, RAND, RAND, RAND, RAND, RAND, RAND, RAND
];

// board 
let board = InitialPosition;

// state
let game = {};

game.turn = 1;
game.enPassant = EMPTY;
game.castlings = [true, true, true, true];
game.kingLocations = [E1, E8];

game.undos = [];

// Move encoding
class Move {
  constructor(from, to, flags, isCapture) {
    this.move = (from & 0xFF) | ((to & 0xFF) << 8) | ((Math.abs(flags) & 0xFF) << 16) | (((isCapture ? 1 : 0) & 0xFF) << 32);
  }
  getFrom() {
    return this.move & 0xFF;
  }
  getTo() {
    return (this.move >> 8) & 0xFF;
  }
  getFlags() {
    return (this.move >> 12) & 0xFF;
  }
  isCapture() {
    return ((this.move >> 32) & 1) === 1;
  }
  isCastling() {
    let rook;
    return [(
      this.getFrom() === E8 && (
        (this.getTo() === G8 && (rook = new Move(H8, F8, 0, false))) ||
        (this.getTo() === C8 && (rook = new Move(A8, D8, 0, false)))
      ) ||
      this.getFrom() === E1 && (
        (this.getTo() === G1 && (rook = new Move(H1, F1, 0, false))) ||
        (this.getTo() === C1 && (rook = new Move(A1, D1, 0, false)))
      )
    ), rook];
  }
  is2Square() {
    return Math.abs(this.getTo() - this.getFrom()) === 20;
  }
}

// Undo encoding
class Undo {
  constructor() {}
  save() {
    this.board = board;
    this.enPassant = game.enPassant;
    this.castlings = game.castlings;
    this.kingLocations = game.kingLocations;
    game.undos.push(this);
  }
  undo() {
    let state = game.undos.pop();

    board = state.board;
    game.enPassant = state.enPassant;
    game.castlings = state.castlings;
    game.kingLocations = state.kingLocations;
  }
}

// tool
function pieceMapper(piece) {
  const pieceMapping = {
    'P': W_P,
    'R': W_R,
    'N': W_N,
    'B': W_B,
    'Q': W_Q,
    'K': W_K,
    'p': B_P,
    'r': B_R,
    'n': B_N,
    'b': B_B,
    'q': B_Q,
    'k': B_K
  };
  return pieceMapping[piece];
}

function pieceReverseMapper(piece) {
  const reverseMapping = {
    [W_P]: 'P',
    [W_R]: 'R',
    [W_N]: 'N',
    [W_B]: 'B',
    [W_Q]: 'Q',
    [W_K]: 'K',
    [B_P]: 'p',
    [B_R]: 'r',
    [B_N]: 'n',
    [B_B]: 'b',
    [B_Q]: 'q',
    [B_K]: 'k'
  };
  return reverseMapping[piece] || '';
}

function pieceColor(piece) {
  return Math.abs(piece) === piece;
}

function squareMapper(idx) {
  const range = [];
  for (let i = 21; i <= 91; i += 10) {
    range.push(i);
  }

  const mapping = {
    1: 'a',
    2: 'b',
    3: 'c',
    4: 'd',
    5: 'e',
    6: 'f',
    7: 'g',
    8: 'h',
  };

  let pair = '';
  for (let i = 0; i < range.length; i++) {
    if (idx >= range[i] && idx <= range[i] + 7) {
      pair = mapping[i + 1] + (idx - range[i] + 1);
      break;
    }
  }

  return pair;
}

function squareReverseMapper(sq) {
  return 8 * (sq[0].charCodeAt(0) - 97) + (parseInt(sq[1], 10) - 1);
}

function loadKingLocations() {
  for (let i = 0; i < 64; i++) {
    if (board[21 + i] === W_K) game.kingLocations[0] = i;
    else if (board[21 + i] === B_K) game.kingLocations[1] = i;
  }
}

// all possible movements

function GenerateWhitePieceMoves(idx) {
  let moves = [];
  switch (board[idx]) {
    case W_P:
      { // Pawn
        if (WHITE_NEAR_PROMOTES.includes(idx)) {
          let promoteSq;
          let isCapture = true;
          if (
            (
              board[idx - 10] === EMPTY &&
              (promoteSq = idx - 10) &&
              (isCapture = false)
            ) || (
              board[idx - 9] !== RAND && board[idx - 9] !== EMPTY &&
              !pieceColor(board[idx - 9]) &&
              (promoteSq = idx - 9)
            ) || (
              board[idx - 11] !== RAND && board[idx - 11] !== EMPTY &&
              !pieceColor(board[idx - 11]) &&
              (promoteSq = idx - 11)
            )
          ) {
            moves.push(new Move(idx, promoteSq, W_Q, isCapture));
            moves.push(new Move(idx, promoteSq, W_N, isCapture));
            moves.push(new Move(idx, promoteSq, W_R, isCapture));
            moves.push(new Move(idx, promoteSq, W_B, isCapture));
          }
        } else {
          if (board[idx - 10] === EMPTY) {
            moves.push(new Move(idx, idx - 10, 0, false));
            if (WHITE_PAWN.includes(idx)) {
              if (board[idx - 20] === EMPTY) {
                moves.push(new Move(idx, idx - 20, 0, false));
              }
            }
            let captureSq;
            if (
              (
                board[idx - 9] !== RAND && (
                  (board[idx - 9] !== EMPTY && !pieceColor(board[idx - 9])) ||
                  (
                    game.enPassant !== EMPTY && (idx - 9) === game.enPassant
                  )) &&
                (captureSq = idx - 9)
              ) || (
                board[idx - 11] !== RAND && (
                  (board[idx - 11] !== EMPTY && !pieceColor(board[idx - 11])) ||
                  (
                    game.enPassant !== EMPTY && (idx - 11) === game.enPassant
                  )) &&
                (captureSq = idx - 11)
              )
            ) {
              moves.push(new Move(idx, captureSq, 0, true));
            }
          }
        }
        break;
      }
    case W_R:
      { // Rook
        let sq = board[idx];
        [+1, -1, +0.1, -0.1].forEach(element => {
          rook: for (let i = 10; i < 80; i += 10) {
            sq = board[idx + (i * element)];
            if (sq === RAND) break rook;
            if (sq !== EMPTY) {
              if (!pieceColor(sq)) {
                moves.push(new Move(idx, idx + i, 0, true));
              }
              break rook;
            }
            moves.push(new Move(idx, idx + i, 0, false));
          }
        });
        break;
      }
    case W_B:
      { // Bishop
        let sq = board[idx];
        [+9, +11, -11, -9].forEach(element => {
          bishop: for (let i = 1; i < 8; i++) {
            sq = board[idx + (i * element)];
            if (sq === RAND) break bishop;
            if (sq !== EMPTY) {
              if (!pieceColor(sq)) {
                moves.push(new Move(idx, idx + i, 0, true));
              }
              break bishop;
            }
            moves.push(new Move(idx, idx + i, 0, false));
          }
        });
        break;
      }
    case W_N:
      { // Knight
        const knightMoves = [-21, -19, -12, -8,
          8, 12, 19, 21
        ];

        for (let i = 0; i < knightMoves.length; i++) {
          let sq = board[idx + knightMoves[i]];

          if (sq === RAND) continue;
          if (sq === EMPTY || !pieceColor(sq)) {
            moves.push(new Move(idx, idx + knightMoves[i], 0, sq !== EMPTY));
          }
        }
        break;
      }
    case W_Q:
      { // Queen
        let sq = board[idx];
        [+1, -1, +0.1, -0.1, +0.9, +0.11, -0.11, -0.9].forEach(element => {
          queen: for (let i = 10; i < 80; i += 10) {
            sq = board[idx + (i * element)];
            if (sq === RAND) break queen;
            if (sq !== EMPTY) {
              if (!pieceColor(sq)) {
                moves.push(new Move(idx, idx + i, 0, true));
              }
              break queen;
            }
            moves.push(new Move(idx, idx + i, 0, false));
          }
        });
        break;
      }
    case W_K:
      { // King
        const kingMoves = [+10, -10, +1, -1, +9, +11, -11, -9];

        for (let i = 0; i < kingMoves.length; i++) {
          let sq = board[idx + kingMoves[i]];

          if (sq === RAND) continue;
          if (sq === EMPTY || !pieceColor(sq)) {
            moves.push(new Move(idx, idx + kingMoves[i], 0, sq !== EMPTY));
          }
        }
        break;
      }
  }
  return moves;
}

function GenerateBlackPieceMoves(idx) {
  let moves = [];
  switch (board[idx]) {
    case B_P:
      { // Pawn
        if (BLACK_NEAR_PROMOTES.includes(idx)) {
          let promiteSq;
          let isCapture = true;
          if (
            (
              board[idx + 10] === EMPTY &&
              (promiteSq = idx + 10) &&
              (isCapture = false)
            ) || (
              board[idx + 9] !== RAND && board[idx + 9] !== EMPTY &&
              pieceColor(board[idx + 9]) &&
              (promiteSq = idx + 9)
            ) || (
              board[idx + 11] !== RAND && board[idx + 11] !== EMPTY &&
              pieceColor(board[idx + 11]) &&
              (promiteSq = idx + 11)
            )
          ) {
            moves.push(new Move(idx, promiteSq, B_Q, isCapture));
            moves.push(new Move(idx, promiteSq, B_N, isCapture));
            moves.push(new Move(idx, promiteSq, B_R, isCapture));
            moves.push(new Move(idx, promiteSq, B_B, isCapture));
          }
        } else {
          if (board[idx + 10] === EMPTY) {
            moves.push(new Move(idx, idx + 10, 0, false));
            if (BLACK_PAWN.includes(idx)) {
              if (board[idx + 20] === EMPTY) {
                moves.push(new Move(idx, idx + 20, 0, false));
              }
            }
            let captureSq;
            if (
              (
                board[idx + 9] !== RAND && (
                  (board[idx + 9] !== EMPTY && pieceColor(board[idx + 9])) ||
                  (
                    game.enPassant !== EMPTY && (idx + 9) === game.enPassant
                  )) &&
                (captureSq = idx + 9)
              ) || (
                board[idx + 11] !== RAND && (
                  (board[idx + 11] !== EMPTY && pieceColor(board[idx + 9])) ||
                  (
                    game.enPassant !== EMPTY && (idx + 11) === game.enPassant
                  )) &&
                (captureSq = idx + 11)
              )
            ) {
              moves.push(new Move(idx, captureSq, 0, true));
            }
          }
        }
        break;
      }
    case B_R:
      { // Rook
        let sq = board[idx];
        [+1, -1, +0.1, -0.1].forEach(element => {
          rook: for (let i = 10; i < 80; i += 10) {
            sq = board[idx + (i * element)];
            if (sq === RAND) break rook;
            if (sq !== EMPTY) {
              if (pieceColor(sq)) {
                moves.push(new Move(idx, idx + i, 0, true));
              }
              break rook;
            }
            moves.push(new Move(idx, idx + i, 0, false));
          }
        });
        break;
      }
    case B_B:
      { // Bishop
        let sq = board[idx];
        [+9, +11, -11, -9].forEach(element => {
          bishop: for (let i = 1; i < 8; i++) {
            sq = board[idx + (i * element)];
            if (sq === RAND) break bishop;
            if (sq !== EMPTY) {
              if (pieceColor(sq)) {
                moves.push(new Move(idx, idx + i, 0, true));
              }
              break bishop;
            }
            moves.push(new Move(idx, idx + i, 0, false));
          }
        });
        break;
      }
    case B_N:
      { // Knight 
        const knightMoves = [-21, -19, -12, -8,
          8, 12, 19, 21
        ];

        for (let i = 0; i < knightMoves.length; i++) {
          let sq = board[idx + knightMoves[i]];

          if (sq === RAND) continue;
          if (sq === EMPTY || pieceColor(sq)) {
            moves.push(new Move(idx, idx + knightMoves[i], 0, sq !== EMPTY));
          }
        }
        break;
      }
    case B_Q:
      { // Queen
        let sq = board[idx];
        [+1, -1, +0.1, -0.1, +0.9, +0.11, -0.11, -0.9].forEach(element => {
          queen: for (let i = 10; i < 80; i += 10) {
            sq = board[idx + (i * element)];
            if (sq === RAND) break queen;
            if (sq !== EMPTY) {
              if (pieceColor(sq)) {
                moves.push(new Move(idx, idx + i, 0, true));
              }
              break queen;
            }
            moves.push(new Move(idx, idx + i, 0, false));
          }
        });
        break;
      }
    case B_K:
      { // King
        const kingMoves = [+10, -10, +1, -1, +9, +11, -11, -9];

        for (let i = 0; i < kingMoves.length; i++) {
          let sq = board[idx + kingMoves[i]];

          if (sq === RAND) continue;
          if (sq === EMPTY || pieceColor(sq)) {
            moves.push(new Move(idx, idx + kingMoves[i], 0, sq !== EMPTY));
          }
        }
        break;
      }
  }
  return moves;
}

// Is an attack possible
function isWhiteAttacked(sq) {
  const blackPieces = [B_P, B_R, B_B, B_N, B_Q, B_K];
  const piece = board[sq];

  for (let pieceType of blackPieces) {
    board[sq] = pieceType;

    const moves = GenerateBlackPieceMoves(sq);
    for (let move of moves) {
      if (board[move.getTo()] !== RAND && !pieceColor(board[move.getTo()])) {
        board[sq] = piece;
        return true;
      }
    }
  }

  board[sq] = piece;
  return false;
}

function isBlackAttacked(sq) {
  const whitePieces = [W_P, W_R, W_B, W_N, W_Q, W_K];
  const piece = board[sq];

  for (let pieceType of whitePieces) {
    board[sq] = pieceType;

    const moves = GenerateWhitePieceMoves(sq);
    for (let move of moves) {
      if (board[move.getTo()] !== RAND && pieceColor(board[move.getTo()])) {
        board[sq] = piece;
        return true;
      }
    }
  }

  board[sq] = piece;
  return false;
}

// All of White's moves (including how many his opponent can check)
function GenerateWhiteMoves() {
  let moves = [];
  if (
    game.castlings[0] &&
    board[G8] === EMPTY &&
    board[F8] === EMPTY &&
    !isBlackAttacked(E1) &&
    !isBlackAttacked(G1) &&
    !isBlackAttacked(F1)
  ) {
    moves.push(new Move(E1, G1, 0, false));
  }
  if (
    game.castlings[1] &&
    board[D1] === EMPTY &&
    board[C1] === EMPTY &&
    board[B1] === EMPTY &&
    !isBlackAttacked(E1) &&
    !isBlackAttacked(D1) &&
    !isBlackAttacked(C1) &&
    !isBlackAttacked(B1)
  ) {
    moves.push(new Move(E1, C1, 0, false));
  }
  for (let i = 0; i < 99; i++) {
    if (board[21 + i] === RAND || board[21 + i] === EMPTY) continue;
    if (pieceColor(board[21 + i])) {
      let pieceMoves = GenerateWhitePieceMoves(21 + i);
      moves.push(...pieceMoves);
    }
  }
  return moves;
}

// All of Black's moves (including how many his opponent can check)
function GenerateBlackMoves() {
  let moves = [];
  if (
    game.castlings[2] &&
    board[G8] === EMPTY &&
    board[F8] === EMPTY &&
    !isWhiteAttacked(E8) &&
    !isWhiteAttacked(G8) &&
    !isWhiteAttacked(F8)
  ) {
    moves.push(new Move(E8, G8, 0, false));
  }
  if (
    game.castlings[3] &&
    board[D8] === EMPTY &&
    board[C8] === EMPTY &&
    board[B8] === EMPTY &&
    !isWhiteAttacked(E8) &&
    !isWhiteAttacked(D8) &&
    !isWhiteAttacked(C8) &&
    !isWhiteAttacked(B8)
  ) {
    moves.push(new Move(E8, C8, 0, false));
  }
  for (let i = 0; i < 99; i++) {
    if (board[21 + i] === RAND || board[21 + i] === EMPTY) continue;
    if (!pieceColor(board[21 + i])) {
      let pieceMoves = GenerateBlackPieceMoves(21 + i);
      moves.push(...pieceMoves);
    }
  }
  return moves;
}

// Create all movements according to the turn
function GenerateAllMoves() {
  return game.turn ? GenerateWhiteMoves() : GenerateBlackMoves()
}

// create movement
function MakeWhiteMove(move) {
  let from = move.getFrom();
  let to = move.getTo();
  let castling = move.isCastling();
  let flag = move.getFlags();
  console.log(squareMapper(from), squareMapper(to));

  if (board[from] === W_K && castling[0]) {
    board[to] = W_K;
    board[from] = EMPTY;
    board[castling[1].getTo()] = W_R;
    board[castling[1].getFrom()] = EMPTY;
  } else if (flag !== EMPTY) {
    board[to] = flag;
    board[from] = EMPTY;
  } else {
    board[to] = board[from];
    board[from] = EMPTY;
    if (board[to] === W_P && move.is2Square()) {
      game.enPassant = to - 10;
    }
  }

  new Undo().save();
}

function MakeBlackMove(move) {
  let from = move.getFrom();
  let to = move.getTo();
  let castling = move.isCastling();
  let flag = move.getFlags();

  if (castling[0]) {
    board[to] = B_K;
    board[from] = EMPTY;
    board[castling[1].getTo()] = B_R;
    board[castling[1].getFrom()] = EMPTY;
  } else if (flag !== EMPTY) {
    board[to] = flag;
    board[from] = EMPTY;
  } else {
    board[to] = board[from];
    board[from] = EMPTY;
    if (board[to] === B_P && move.is2Square()) {
      game.enPassant = to + 10;
    }
  }

  new Undo().save();
}

// FEN
function fenToMailbox(fen) {
  const [position, turn, castling, enPassant] = fen.split(' ');

  // Initializing mailbox array
  const mailboxBoard = new Array(BOARD_IDX).fill(EMPTY);
  const boardRanks = position.split('/');

  let index = 21;
  boardRanks.forEach(rank => {
    for (let i = 0; i < rank.length; i++) {
      const symbol = rank[i];
      if (isNaN(+symbol)) { // If the symbol is not a number
        mailboxBoard[index] = pieceMapper(symbol);
        index++;
      } else {
        index += parseInt(symbol, 10);
      }
    }
    index += 2;
  });

  // Filling the padding space
  for (let i = 0; i < 10; i++) {
    // top
    mailboxBoard[i] = RAND;
    mailboxBoard[10 + i] = RAND;

    // bottom
    mailboxBoard[100 + i] = RAND;
    mailboxBoard[110 + i] = RAND;
  }
  for (let i = 0; i < 100; i = i + 10) {
    // left
    mailboxBoard[20 + i] = RAND;

    // right
    mailboxBoard[29 + i] = RAND;
  }

  game.turn = turn === "w" ? 1 : 0;
  game.enPassant = enPassant === '-' ? EMPTY : squareReverseMapper(enPassant);
  loadKingLocations();

  return mailboxBoard;
}

function mailboxToFen(mailboxBoard) {
  let fen = '';
  let emptySquares = 0;

  for (let i = 21; i < 99; i++) {
    const piece = mailboxBoard[i];

    if (piece === RAND) {
      continue; // Ignore border cells
    }

    if (piece === EMPTY) {
      emptySquares++;
    } else {
      if (emptySquares > 0) {
        fen += emptySquares;
        emptySquares = 0;
      }
      fen += pieceReverseMapper(piece);
    }

    if (i % 10 === 8) { // end of rank
      if (emptySquares > 0) {
        fen += emptySquares;
        emptySquares = 0;
      }
      if (i < 98) {
        fen += '/';
      }
      i += 2; // Skip border cells
    }
  }

  fen += " " + (game.turn ? "w" : "b");

  // todo: Additional FEN information (castling, en passant, etc.) should be added here.
  // For now we simply transform the positions of the pieces.

  return fen;
}

const fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
const mailboxBoard = fenToMailbox(fen);
board = mailboxBoard;


let loop = 100000;
let startTime = performance.now()
for (let i = 1; i <= loop; i++) {
  GenerateAllMoves();
}
let endTime = performance.now()
console.log((endTime - startTime) / loop);

GenerateAllMoves().forEach(move => {
  console.log(squareMapper(move.getFrom()), squareMapper(move.getTo()));
})

Current results:

g1 f1
g1 e1
g2 f2
g2 e2
g3 f3
g3 e3
g4 f4
g4 e4
g5 f5
g5 e5
g6 f6
g6 e6
g7 f7
g7 e7
g8 f8
g8 e8
h2 f1
h2 f3
h5
h5
h7 f6
h7 f8

Expected result:

a2 a3
a2 a4
b2 b3
b2 b4
c2 c3
c2 c4
d2 d3
d2 d4
e2 e3
e2 e4
f2 f3
f2 f4
g2 g3
g2 g4
h2 h3
h2 h4
b1 a3
b1 c3
g1 f3
g1 h3

=====================

Completed:

function squareMapper(idx){
    const mapping = {
        1: 'a',
        2: 'b',
        3: 'c',
        4: 'd',
        5: 'e',
        6: 'f',
        7: 'g',
        8: 'h',
    };

    let column = mapping[idx % 10];  

    let row = 10 - Math.floor(idx / 10);

    return `${column}${row}`;
}

function squareReverseMapper(sq) {
    const mapping = {
        'a': 1,
        'b': 2,
        'c': 3,
        'd': 4,
        'e': 5,
        'f': 6,
        'g': 7,
        'h': 8
    };

    let column = mapping[sq[0]];
    let row = sq[1];

    return (10 - row) * 10 + column;
}

Solution

  • One problem is with your squareMapping function

    As I understand your code all indexes with the same unit position (=last digit for instance the 1 in 21, 31, 41, ...) should be in the same column (ie A for all 21, 31...) But with your code

    pair = mapping[i + 1] + ...
    

    You are actually making it depend on the decade position (=first digit, for instance 6 in 61, 62, 63). Because i counts through the ranges, ie if your idx == 81 it is between 81 and 90 your i will be 6 thus you are picking up letter g instead of a

    And you also calculate your row wrongly, because when you do

    pair = ... + (idx - range[i] + 1)
    

    you are actually letting the row depend on the column. As I understand your code, idx for instance 81 and 83 should be in the same row (ie row 2) but with your calculation, 81 will be in row 81 - 81 + 1 == 1 (which is wrong at its own) and 83 will be in row 83 - 81 + 1 == 3 which is also wrong ...

    So actually, in your squareMapping, you have switched rows and columns. Thus, your game seems to be going left <-> right instead of top <-> down

    So the correct method to calculate the square mapping could be as follows (assuming I understood your code correctly)

    //the column only depends on the last digit, 1 = a, 2 = b, 3 = c ...
    let column = mapping[idx % 10];  
    
    //or even simpler without the need for an explicit mapping object
    //let a = "a".charCodeAt(0); //97 in ASCII or UTF8
    //let column = String.fromCharCode(a + idx % 10 - 1);
    
    
    //the row only depends on the first digit, 9x = 1, 8x = 2, 7x= 3, ... 
    let row = 10 - Math.floor(idx / 10);
    pair = `${column}${row}`;
    

    If you do it like this, your algorithm prints out the correct moves (at least for the first few pieces, didn't check all of it)

    Also your reverseSquareMapper seems strange, for instance for an input of a1 it returns 0 which is obviously wrong ...

    A correct calulation (based on my above assumptions and squareMapping function) could be

    let idx = (100 - (+sq[1]) * 10) //calculates the decade, row1= 9x, row2 = 8x, row3 = 7x ....
              + (sq.charCodeAt(0) - 96) //calculate the unit, a = 1, b = 2, c = 3, ...