ai-chess/script.js
2024-02-20 17:32:41 +05:30

1041 lines
44 KiB
JavaScript

"use strict";
console.clear();
let PIECE_DIR_CALC = 0;
class Utils {
static colToInt(col) {
return Board.COLS.indexOf(col);
}
static rowToInt(row) {
return Board.ROWS.indexOf(row);
}
static intToCol(int) {
return Board.COLS[int];
}
static intToRow(int) {
return Board.ROWS[int];
}
static getPositionsFromShortCode(shortCode) {
const positions = Utils.getInitialPiecePositions();
const overrides = {};
const defaultPositionMode = shortCode.charAt(0) === "X";
if (defaultPositionMode) {
shortCode = shortCode.slice(1);
}
shortCode.split(",").forEach((string) => {
const promoted = string.charAt(0) === "P";
if (promoted) {
string = string.slice(1);
}
if (defaultPositionMode) {
const inactive = string.length === 3;
const id = string.slice(0, 2);
const col = inactive ? undefined : string.charAt(2);
const row = inactive ? undefined : string.charAt(3);
const moves = string.charAt(4) || "1";
overrides[id] = {
col,
row,
active: !inactive,
_moves: parseInt(moves),
_promoted: promoted,
};
}
else {
const moved = string.length >= 4;
const id = string.slice(0, 2);
const col = string.charAt(moved ? 2 : 0);
const row = string.charAt(moved ? 3 : 1);
const moves = string.charAt(4) || moved ? "1" : "0";
overrides[id] = { col, row, active: true, _moves: parseInt(moves), _promoted: promoted };
}
});
for (let id in positions) {
if (overrides[id]) {
positions[id] = overrides[id];
}
else {
positions[id] = defaultPositionMode ? positions[id] : { active: false };
}
}
return positions;
}
static getInitialBoardPieces(parent, pieces) {
const boardPieces = {};
const container = document.createElement("div");
container.className = "pieces";
parent.appendChild(container);
for (let pieceId in pieces) {
const boardPiece = document.createElement("div");
boardPiece.className = `piece ${pieces[pieceId].data.player.toLowerCase()}`;
boardPiece.innerHTML = pieces[pieceId].shape();
container.appendChild(boardPiece);
boardPieces[pieceId] = boardPiece;
}
return boardPieces;
}
static getInitialBoardTiles(parent, handler) {
const tiles = { 1: {}, 2: {}, 3: {}, 4: {}, 5: {}, 6: {}, 7: {}, 8: {} };
const board = document.createElement("div");
board.className = "board";
parent.appendChild(board);
for (let i = 0; i < 8; i++) {
const row = document.createElement("div");
row.className = "row";
board.appendChild(row);
for (let j = 0; j < 8; j++) {
const tile = document.createElement("button");
tile.className = "tile";
const r = Utils.intToRow(i);
const c = Utils.intToCol(j);
tile.addEventListener("click", () => handler({ row: r, col: c }));
row.appendChild(tile);
tiles[r][c] = tile;
}
}
return tiles;
}
static getInitialBoardState(construct = () => undefined) {
const blankRow = () => ({
A: construct(),
B: construct(),
C: construct(),
D: construct(),
E: construct(),
F: construct(),
G: construct(),
H: construct(),
});
return {
1: Object.assign({}, blankRow()),
2: Object.assign({}, blankRow()),
3: Object.assign({}, blankRow()),
4: Object.assign({}, blankRow()),
5: Object.assign({}, blankRow()),
6: Object.assign({}, blankRow()),
7: Object.assign({}, blankRow()),
8: Object.assign({}, blankRow()),
};
}
static getInitialPiecePositions() {
return {
A8: { active: true, row: "8", col: "A" },
B8: { active: true, row: "8", col: "B" },
C8: { active: true, row: "8", col: "C" },
D8: { active: true, row: "8", col: "D" },
E8: { active: true, row: "8", col: "E" },
F8: { active: true, row: "8", col: "F" },
G8: { active: true, row: "8", col: "G" },
H8: { active: true, row: "8", col: "H" },
A7: { active: true, row: "7", col: "A" },
B7: { active: true, row: "7", col: "B" },
C7: { active: true, row: "7", col: "C" },
D7: { active: true, row: "7", col: "D" },
E7: { active: true, row: "7", col: "E" },
F7: { active: true, row: "7", col: "F" },
G7: { active: true, row: "7", col: "G" },
H7: { active: true, row: "7", col: "H" },
A2: { active: true, row: "2", col: "A" },
B2: { active: true, row: "2", col: "B" },
C2: { active: true, row: "2", col: "C" },
D2: { active: true, row: "2", col: "D" },
E2: { active: true, row: "2", col: "E" },
F2: { active: true, row: "2", col: "F" },
G2: { active: true, row: "2", col: "G" },
H2: { active: true, row: "2", col: "H" },
A1: { active: true, row: "1", col: "A" },
B1: { active: true, row: "1", col: "B" },
C1: { active: true, row: "1", col: "C" },
D1: { active: true, row: "1", col: "D" },
E1: { active: true, row: "1", col: "E" },
F1: { active: true, row: "1", col: "F" },
G1: { active: true, row: "1", col: "G" },
H1: { active: true, row: "1", col: "H" },
};
}
static getInitialPieces() {
return {
A8: new Piece({ id: "A8", player: "BLACK", type: "ROOK" }),
B8: new Piece({ id: "B8", player: "BLACK", type: "KNIGHT" }),
C8: new Piece({ id: "C8", player: "BLACK", type: "BISHOP" }),
D8: new Piece({ id: "D8", player: "BLACK", type: "QUEEN" }),
E8: new Piece({ id: "E8", player: "BLACK", type: "KING" }),
F8: new Piece({ id: "F8", player: "BLACK", type: "BISHOP" }),
G8: new Piece({ id: "G8", player: "BLACK", type: "KNIGHT" }),
H8: new Piece({ id: "H8", player: "BLACK", type: "ROOK" }),
A7: new Piece({ id: "A7", player: "BLACK", type: "PAWN" }),
B7: new Piece({ id: "B7", player: "BLACK", type: "PAWN" }),
C7: new Piece({ id: "C7", player: "BLACK", type: "PAWN" }),
D7: new Piece({ id: "D7", player: "BLACK", type: "PAWN" }),
E7: new Piece({ id: "E7", player: "BLACK", type: "PAWN" }),
F7: new Piece({ id: "F7", player: "BLACK", type: "PAWN" }),
G7: new Piece({ id: "G7", player: "BLACK", type: "PAWN" }),
H7: new Piece({ id: "H7", player: "BLACK", type: "PAWN" }),
A2: new Piece({ id: "A2", player: "WHITE", type: "PAWN" }),
B2: new Piece({ id: "B2", player: "WHITE", type: "PAWN" }),
C2: new Piece({ id: "C2", player: "WHITE", type: "PAWN" }),
D2: new Piece({ id: "D2", player: "WHITE", type: "PAWN" }),
E2: new Piece({ id: "E2", player: "WHITE", type: "PAWN" }),
F2: new Piece({ id: "F2", player: "WHITE", type: "PAWN" }),
G2: new Piece({ id: "G2", player: "WHITE", type: "PAWN" }),
H2: new Piece({ id: "H2", player: "WHITE", type: "PAWN" }),
A1: new Piece({ id: "A1", player: "WHITE", type: "ROOK" }),
B1: new Piece({ id: "B1", player: "WHITE", type: "KNIGHT" }),
C1: new Piece({ id: "C1", player: "WHITE", type: "BISHOP" }),
D1: new Piece({ id: "D1", player: "WHITE", type: "QUEEN" }),
E1: new Piece({ id: "E1", player: "WHITE", type: "KING" }),
F1: new Piece({ id: "F1", player: "WHITE", type: "BISHOP" }),
G1: new Piece({ id: "G1", player: "WHITE", type: "KNIGHT" }),
H1: new Piece({ id: "H1", player: "WHITE", type: "ROOK" }),
};
}
}
class Shape {
static shape(player, piece) {
return `<svg class="${player}" width="170" height="170" viewBox="0 0 170 170" fill="none" xmlns="http://www.w3.org/2000/svg">
<use href="#${piece}" />
</svg>`;
}
static shapeBishop(player) {
return Shape.shape(player, "bishop");
}
static shapeKing(player) {
return Shape.shape(player, "king");
}
static shapeKnight(player) {
return Shape.shape(player, "knight");
}
static shapePawn(player) {
return Shape.shape(player, "pawn");
}
static shapeQueen(player) {
return Shape.shape(player, "queen");
}
static shapeRook(player) {
return Shape.shape(player, "rook");
}
}
class Constraints {
static generate(args, resultingChecks) {
let method;
const { piecePositions, piece } = args;
if (piecePositions[piece.data.id].active) {
switch (piece.data.type) {
case "BISHOP":
method = Constraints.constraintsBishop;
break;
case "KING":
method = Constraints.constraintsKing;
break;
case "KNIGHT":
method = Constraints.constraintsKnight;
break;
case "PAWN":
method = Constraints.constraintsPawn;
break;
case "QUEEN":
method = Constraints.constraintsQueen;
break;
case "ROOK":
method = Constraints.constraintsRook;
break;
}
}
const result = method ? method(args) : { moves: [], captures: [] };
if (resultingChecks) {
const moveIndex = args.moveIndex + 1;
result.moves = result.moves.filter((location) => !resultingChecks({ piece, location, capture: false, moveIndex }).length);
result.captures = result.captures.filter((location) => !resultingChecks({ piece, location, capture: true, moveIndex }).length);
}
return result;
}
static constraintsBishop(args) {
return Constraints.constraintsDiagonal(args);
}
static constraintsDiagonal(args) {
const response = { moves: [], captures: [] };
const { piece } = args;
Constraints.runUntil(piece.dirNW.bind(piece), response, args);
Constraints.runUntil(piece.dirNE.bind(piece), response, args);
Constraints.runUntil(piece.dirSW.bind(piece), response, args);
Constraints.runUntil(piece.dirSE.bind(piece), response, args);
return response;
}
static constraintsKing(args) {
const { piece, kingCastles, piecePositions } = args;
const moves = [];
const captures = [];
const locations = [
piece.dirN(1, piecePositions),
piece.dirNE(1, piecePositions),
piece.dirE(1, piecePositions),
piece.dirSE(1, piecePositions),
piece.dirS(1, piecePositions),
piece.dirSW(1, piecePositions),
piece.dirW(1, piecePositions),
piece.dirNW(1, piecePositions),
];
if (kingCastles) {
const castles = kingCastles(piece);
castles.forEach((position) => moves.push(position));
}
locations.forEach((location) => {
const value = Constraints.relationshipToTile(location, args);
if (value === "BLANK") {
moves.push(location);
}
else if (value === "ENEMY") {
captures.push(location);
}
});
return { moves, captures };
}
static constraintsKnight(args) {
const { piece, piecePositions } = args;
const moves = [];
const captures = [];
const locations = [
piece.dir(1, 2, piecePositions),
piece.dir(1, -2, piecePositions),
piece.dir(2, 1, piecePositions),
piece.dir(2, -1, piecePositions),
piece.dir(-1, 2, piecePositions),
piece.dir(-1, -2, piecePositions),
piece.dir(-2, 1, piecePositions),
piece.dir(-2, -1, piecePositions),
];
locations.forEach((location) => {
const value = Constraints.relationshipToTile(location, args);
if (value === "BLANK") {
moves.push(location);
}
else if (value === "ENEMY") {
captures.push(location);
}
});
return { moves, captures };
}
static constraintsOrthangonal(args) {
const { piece } = args;
const response = { moves: [], captures: [] };
Constraints.runUntil(piece.dirN.bind(piece), response, args);
Constraints.runUntil(piece.dirE.bind(piece), response, args);
Constraints.runUntil(piece.dirS.bind(piece), response, args);
Constraints.runUntil(piece.dirW.bind(piece), response, args);
return response;
}
static constraintsPawn(args) {
const { piece, piecePositions } = args;
const moves = [];
const captures = [];
const locationN1 = piece.dirN(1, piecePositions);
const locationN2 = piece.dirN(2, piecePositions);
if (Constraints.relationshipToTile(locationN1, args) === "BLANK") {
moves.push(locationN1);
if (!piece.moves.length && Constraints.relationshipToTile(locationN2, args) === "BLANK") {
moves.push(locationN2);
}
}
[
[piece.dirNW(1, piecePositions), piece.dirW(1, piecePositions)],
[piece.dirNE(1, piecePositions), piece.dirE(1, piecePositions)],
].forEach(([location, enPassant]) => {
const standardCaptureRelationship = Constraints.relationshipToTile(location, args);
const enPassantCaptureRelationship = Constraints.relationshipToTile(enPassant, args);
if (standardCaptureRelationship === "ENEMY") {
captures.push(location);
}
else if (piece.moves.length > 0 && enPassantCaptureRelationship === "ENEMY") {
const enPassantRow = enPassant.row === (piece.playerWhite() ? "5" : "4");
const other = Constraints.locationToPiece(enPassant, args);
if (enPassantRow && other && other.data.type === "PAWN") {
if (other.moves.length === 1 && other.moves[0] === args.moveIndex - 1) {
location.capture = Object.assign({}, enPassant);
captures.push(location);
}
}
}
});
return { moves, captures };
}
static constraintsQueen(args) {
const diagonal = Constraints.constraintsDiagonal(args);
const orthagonal = Constraints.constraintsOrthangonal(args);
return {
moves: diagonal.moves.concat(orthagonal.moves),
captures: diagonal.captures.concat(orthagonal.captures),
};
}
static constraintsRook(args) {
return Constraints.constraintsOrthangonal(args);
}
static locationToPiece(location, args) {
if (!location) {
return undefined;
}
const { state, pieces } = args;
const row = state[location.row];
const occupyingId = row === undefined ? undefined : row[location.col];
return pieces[occupyingId];
}
static relationshipToTile(location, args) {
if (!location) {
return undefined;
}
const { piece } = args;
const occupying = Constraints.locationToPiece(location, args);
if (occupying) {
return occupying.data.player === piece.data.player ? "FRIEND" : "ENEMY";
}
else {
return "BLANK";
}
}
static runUntil(locationFunction, response, args) {
const { piecePositions } = args;
let inc = 1;
let location = locationFunction(inc++, piecePositions);
while (location) {
let abort = false;
const relations = Constraints.relationshipToTile(location, args);
if (relations === "ENEMY") {
response.captures.push(location);
abort = true;
}
else if (relations === "FRIEND") {
abort = true;
}
else {
response.moves.push(location);
}
if (abort) {
location = undefined;
}
else {
location = locationFunction(inc++, piecePositions);
}
}
}
}
class Piece {
constructor(data) {
this.moves = [];
this.promoted = false;
this.updateShape = false;
this.data = data;
}
get orientation() {
return this.data.player === "BLACK" ? -1 : 1;
}
dirN(steps, positions) {
return this.dir(steps, 0, positions);
}
dirS(steps, positions) {
return this.dir(-steps, 0, positions);
}
dirW(steps, positions) {
return this.dir(0, -steps, positions);
}
dirE(steps, positions) {
return this.dir(0, steps, positions);
}
dirNW(steps, positions) {
return this.dir(steps, -steps, positions);
}
dirNE(steps, positions) {
return this.dir(steps, steps, positions);
}
dirSW(steps, positions) {
return this.dir(-steps, -steps, positions);
}
dirSE(steps, positions) {
return this.dir(-steps, steps, positions);
}
dir(stepsRow, stepsColumn, positions) {
PIECE_DIR_CALC++;
const row = Utils.rowToInt(positions[this.data.id].row) + this.orientation * stepsRow;
const col = Utils.colToInt(positions[this.data.id].col) + this.orientation * stepsColumn;
if (row >= 0 && row <= 7 && col >= 0 && col <= 7) {
return { row: Utils.intToRow(row), col: Utils.intToCol(col) };
}
return undefined;
}
move(moveIndex) {
this.moves.push(moveIndex);
}
options(moveIndex, state, pieces, piecePositions, resultingChecks, kingCastles) {
return Constraints.generate({ moveIndex, state, piece: this, pieces, piecePositions, kingCastles }, resultingChecks);
}
playerBlack() {
return this.data.player === "BLACK";
}
playerWhite() {
return this.data.player === "WHITE";
}
promote(type = "QUEEN") {
this.data.type = type;
this.promoted = true;
this.updateShape = true;
}
shape() {
const player = this.data.player.toLowerCase();
switch (this.data.type) {
case "BISHOP":
return Shape.shapeBishop(player);
case "KING":
return Shape.shapeKing(player);
case "KNIGHT":
return Shape.shapeKnight(player);
case "PAWN":
return Shape.shapePawn(player);
case "QUEEN":
return Shape.shapeQueen(player);
case "ROOK":
return Shape.shapeRook(player);
}
}
}
class Board {
constructor(pieces, piecePositions) {
this.checksBlack = [];
this.checksWhite = [];
this.piecesTilesCaptures = {};
this.piecesTilesMoves = {};
this.tilesPiecesBlackCaptures = Utils.getInitialBoardState(() => []);
this.tilesPiecesBlackMoves = Utils.getInitialBoardState(() => []);
this.tilesPiecesWhiteCaptures = Utils.getInitialBoardState(() => []);
this.tilesPiecesWhiteMoves = Utils.getInitialBoardState(() => []);
this.pieceIdsBlack = [];
this.pieceIdsWhite = [];
this.state = Utils.getInitialBoardState();
this.pieces = pieces;
for (let id in pieces) {
if (pieces[id].playerWhite()) {
this.pieceIdsWhite.push(id);
}
else {
this.pieceIdsBlack.push(id);
}
}
this.initializePositions(piecePositions);
}
initializePositions(piecePositions) {
this.piecePositions = piecePositions;
this.initializeState();
this.piecesUpdate(0);
}
initializeState() {
for (let pieceId in this.pieces) {
const { row, col, active, _moves, _promoted } = this.piecePositions[pieceId];
if (_moves) {
delete this.piecePositions[pieceId]._moves;
// TODO: come back to this
// this.pieces[pieceId].moves = new Array(_moves);
}
if (_promoted) {
delete this.piecePositions[pieceId]._promoted;
this.pieces[pieceId].promote();
}
if (active) {
this.state[row] = this.state[row] || [];
this.state[row][col] = pieceId;
}
}
}
kingCastles(king) {
const castles = [];
// king has to not have moved
if (king.moves.length) {
return castles;
}
const kingIsWhite = king.playerWhite();
const moves = kingIsWhite ? this.tilesPiecesBlackMoves : this.tilesPiecesWhiteMoves;
const checkPositions = (row, rookCol, castles) => {
const cols = rookCol === "A" ? ["D", "C", "B"] : ["F", "G"];
// rook has to not have moved
const rookId = `${rookCol}${row}`;
const rook = this.pieces[rookId];
const { active } = this.piecePositions[rookId];
if (active && rook.moves.length === 0) {
let canCastle = true;
cols.forEach((col) => {
// each tile has to be empty
if (this.state[row][col]) {
canCastle = false;
// each tile cant be in the path of the other team
}
else if (moves[row][col].length) {
canCastle = false;
}
});
if (canCastle) {
castles.push({ col: cols[1], row, castles: rookCol });
}
}
};
const row = kingIsWhite ? "1" : "8";
if (!this.pieces[`A${row}`].moves.length) {
checkPositions(row, "A", castles);
}
if (!this.pieces[`H${row}`].moves.length) {
checkPositions(row, "H", castles);
}
return castles;
}
kingCheckStates(kingPosition, captures, piecePositions) {
const { col, row } = kingPosition;
return captures[row][col].map((id) => piecePositions[id]).filter((pos) => pos.active);
}
pieceCalculateMoves(pieceId, moveIndex, state, piecePositions, piecesTilesCaptures, piecesTilesMoves, tilesPiecesCaptures, tilesPiecesMoves, resultingChecks, kingCastles) {
const { captures, moves } = this.pieces[pieceId].options(moveIndex, state, this.pieces, piecePositions, resultingChecks, kingCastles);
piecesTilesCaptures[pieceId] = Array.from(captures);
piecesTilesMoves[pieceId] = Array.from(moves);
captures.forEach(({ col, row }) => tilesPiecesCaptures[row][col].push(pieceId));
moves.forEach(({ col, row }) => tilesPiecesMoves[row][col].push(pieceId));
}
pieceCapture(piece) {
const pieceId = piece.data.id;
const { col, row } = this.piecePositions[pieceId];
this.state[row][col] = undefined;
delete this.piecePositions[pieceId].col;
delete this.piecePositions[pieceId].row;
this.piecePositions[pieceId].active = false;
}
pieceMove(piece, location) {
const pieceId = piece.data.id;
const { row, col } = this.piecePositions[pieceId];
this.state[row][col] = undefined;
this.state[location.row][location.col] = pieceId;
this.piecePositions[pieceId].row = location.row;
this.piecePositions[pieceId].col = location.col;
if (piece.data.type === "PAWN" && (location.row === "8" || location.row === "1")) {
piece.promote();
}
}
piecesUpdate(moveIndex) {
this.tilesPiecesBlackCaptures = Utils.getInitialBoardState(() => []);
this.tilesPiecesBlackMoves = Utils.getInitialBoardState(() => []);
this.tilesPiecesWhiteCaptures = Utils.getInitialBoardState(() => []);
this.tilesPiecesWhiteMoves = Utils.getInitialBoardState(() => []);
this.pieceIdsBlack.forEach((id) => this.pieceCalculateMoves(id, moveIndex, this.state, this.piecePositions, this.piecesTilesCaptures, this.piecesTilesMoves, this.tilesPiecesBlackCaptures, this.tilesPiecesBlackMoves, this.resultingChecks.bind(this), this.kingCastles.bind(this)));
this.pieceIdsWhite.forEach((id) => this.pieceCalculateMoves(id, moveIndex, this.state, this.piecePositions, this.piecesTilesCaptures, this.piecesTilesMoves, this.tilesPiecesWhiteCaptures, this.tilesPiecesWhiteMoves, this.resultingChecks.bind(this), this.kingCastles.bind(this)));
this.checksBlack = this.kingCheckStates(this.piecePositions.E1, this.tilesPiecesBlackCaptures, this.piecePositions);
this.checksWhite = this.kingCheckStates(this.piecePositions.E8, this.tilesPiecesWhiteCaptures, this.piecePositions);
}
resultingChecks({ piece, location, capture, moveIndex }) {
const tilesPiecesCaptures = Utils.getInitialBoardState(() => []);
const tilesPiecesMoves = Utils.getInitialBoardState(() => []);
const piecesTilesCaptures = {};
const piecesTilesMoves = {};
const state = JSON.parse(JSON.stringify(this.state));
const piecePositions = JSON.parse(JSON.stringify(this.piecePositions));
if (capture) {
const loc = location.capture || location;
const capturedId = state[loc.row][loc.col];
if (this.pieces[capturedId].data.type === "KING") {
// this is a checking move
}
else {
delete piecePositions[capturedId].col;
delete piecePositions[capturedId].row;
piecePositions[capturedId].active = false;
}
}
const pieceId = piece.data.id;
const { row, col } = piecePositions[pieceId];
state[row][col] = undefined;
state[location.row][location.col] = pieceId;
piecePositions[pieceId].row = location.row;
piecePositions[pieceId].col = location.col;
const ids = piece.playerWhite() ? this.pieceIdsBlack : this.pieceIdsWhite;
const king = piece.playerWhite() ? piecePositions.E1 : piecePositions.E8;
ids.forEach((id) => this.pieceCalculateMoves(id, moveIndex, state, piecePositions, piecesTilesCaptures, piecesTilesMoves, tilesPiecesCaptures, tilesPiecesMoves));
return this.kingCheckStates(king, tilesPiecesCaptures, piecePositions);
}
tileEach(callback) {
Board.ROWS.forEach((row) => {
Board.COLS.forEach((col) => {
const piece = this.tileFind({ row, col });
const moves = piece ? this.piecesTilesMoves[piece.data.id] : undefined;
const captures = piece ? this.piecesTilesCaptures[piece.data.id] : undefined;
callback({ row, col }, piece, moves, captures);
});
});
}
tileFind({ row, col }) {
const id = this.state[row][col];
return this.pieces[id];
}
toShortCode() {
const positionsAbsolute = [];
const positionsDefaults = [];
for (let id in this.piecePositions) {
const { active, col, row } = this.piecePositions[id];
const pos = `${col}${row}`;
const moves = this.pieces[id].moves;
const promotedCode = this.pieces[id].promoted ? "P" : "";
const movesCode = moves > 9 ? "9" : moves > 1 ? moves.toString() : "";
if (active) {
positionsAbsolute.push(`${promotedCode}${id}${id === pos ? "" : pos}${movesCode}`);
if (id !== pos || moves > 0) {
positionsDefaults.push(`${promotedCode}${id}${pos}${movesCode}`);
}
}
else {
if (id !== "BQ" && id !== "WQ") {
positionsDefaults.push(`${promotedCode}${id}X`);
}
}
}
const pA = positionsAbsolute.join(",");
const pD = positionsDefaults.join(",");
return pA.length > pD.length ? `X${pD}` : pA;
}
}
Board.COLS = ["A", "B", "C", "D", "E", "F", "G", "H"];
Board.ROWS = ["1", "2", "3", "4", "5", "6", "7", "8"];
class Game {
constructor(pieces, piecePositions, turn = "WHITE") {
this.active = null;
this.activePieceOptions = [];
this.moveIndex = 0;
this.moves = [];
this.turn = turn;
this.board = new Board(pieces, piecePositions);
}
activate(location) {
const tilePiece = this.board.tileFind(location);
if (tilePiece && !this.active && tilePiece.data.player !== this.turn) {
this.active = null;
return { type: "INVALID" };
// a piece is active rn
}
else if (this.active) {
const activePieceId = this.active.data.id;
this.active = null;
const validatedPosition = this.activePieceOptions.find((option) => option.col === location.col && option.row === location.row);
const positionIsValid = !!validatedPosition;
this.activePieceOptions = [];
const capturePiece = (validatedPosition === null || validatedPosition === void 0 ? void 0 : validatedPosition.capture) ? this.board.tileFind(validatedPosition.capture) : tilePiece;
// a piece is on the tile
if (capturePiece) {
const capturedPieceId = capturePiece.data.id;
// cancelling the selected piece on invalid location
if (capturedPieceId === activePieceId) {
return { type: "CANCEL" };
}
else if (positionIsValid) {
// capturing the selected piece
this.capture(activePieceId, capturedPieceId, location);
return {
type: "CAPTURE",
activePieceId,
capturedPieceId,
captures: [location],
};
// cancel
}
else if (capturePiece.data.player !== this.turn) {
return { type: "CANCEL" };
}
else {
// proceed to TOUCH or CANCEL
}
}
else if (positionIsValid) {
// moving will return castled if that happens (only two move)
const castledId = this.move(activePieceId, location);
return { type: "MOVE", activePieceId, moves: [location], castledId };
// invalid spot. cancel.
}
else {
return { type: "CANCEL" };
}
}
// no piece selected or new CANCEL + TOUCH
if (tilePiece) {
const tilePieceId = tilePiece.data.id;
const moves = this.board.piecesTilesMoves[tilePieceId];
const captures = this.board.piecesTilesCaptures[tilePieceId];
if (!moves.length && !captures.length) {
return { type: "INVALID" };
}
this.active = tilePiece;
this.activePieceOptions = moves.concat(captures);
return { type: "TOUCH", captures, moves, activePieceId: tilePieceId };
// cancelling
}
else {
this.activePieceOptions = [];
return { type: "CANCEL" };
}
}
capture(capturingPieceId, capturedPieceId, location) {
const captured = this.board.pieces[capturedPieceId];
this.board.pieceCapture(captured);
this.move(capturingPieceId, location, true);
}
handleCastling(piece, location) {
if (piece.data.type !== "KING" ||
piece.moves.length ||
location.row !== (piece.playerWhite() ? "1" : "8") ||
(location.col !== "C" && location.col !== "G")) {
return;
}
return `${location.col === "C" ? "A" : "H"}${location.row}`;
}
move(pieceId, location, capture = false) {
const piece = this.board.pieces[pieceId];
const castledId = this.handleCastling(piece, location);
piece.move(this.moveIndex);
if (castledId) {
const castled = this.board.pieces[castledId];
castled.move(this.moveIndex);
this.board.pieceMove(castled, { col: location.col === "C" ? "D" : "F", row: location.row });
this.moves.push(`${pieceId}O${location.col}${location.row}`);
}
else {
this.moves.push(`${pieceId}${capture ? "x" : ""}${location.col}${location.row}`);
}
this.moveIndex++;
this.board.pieceMove(piece, location);
this.turn = this.turn === "WHITE" ? "BLACK" : "WHITE";
this.board.piecesUpdate(this.moveIndex);
const state = this.moveResultState();
console.log(state);
if (!state.moves && !state.captures) {
alert(state.stalemate ? "Stalemate!" : `${this.turn === "WHITE" ? "Black" : "White"} Wins!`);
}
return castledId;
}
moveResultState() {
let movesWhite = 0;
let capturesWhite = 0;
let movesBlack = 0;
let capturesBlack = 0;
this.board.tileEach(({ row, col }) => {
movesWhite += this.board.tilesPiecesWhiteMoves[row][col].length;
capturesWhite += this.board.tilesPiecesWhiteCaptures[row][col].length;
movesBlack += this.board.tilesPiecesBlackMoves[row][col].length;
capturesBlack += this.board.tilesPiecesBlackCaptures[row][col].length;
});
const activeBlack = this.board.pieceIdsBlack.filter((pieceId) => this.board.piecePositions[pieceId].active).length;
const activeWhite = this.board.pieceIdsWhite.filter((pieceId) => this.board.piecePositions[pieceId].active).length;
const moves = this.turn === "WHITE" ? movesWhite : movesBlack;
const captures = this.turn === "WHITE" ? capturesWhite : capturesBlack;
const noMoves = movesWhite + capturesWhite + movesBlack + capturesBlack === 0;
const checked = !!this.board[this.turn === "WHITE" ? "checksBlack" : "checksWhite"].length;
const onlyKings = activeBlack === 1 && activeWhite === 1;
const stalemate = onlyKings || noMoves || ((moves + captures === 0) && !checked);
const code = this.board.toShortCode();
return { turn: this.turn, checked, moves, captures, code, stalemate };
}
randomMove() {
if (this.active) {
if (this.activePieceOptions.length) {
const { col, row } = this.activePieceOptions[Math.floor(Math.random() * this.activePieceOptions.length)];
return { col, row };
}
else {
const { col, row } = this.board.piecePositions[this.active.data.id];
return { col, row };
}
}
else {
const ids = this.turn === "WHITE" ? this.board.pieceIdsWhite : this.board.pieceIdsBlack;
const positions = ids.map((pieceId) => {
const moves = this.board.piecesTilesMoves[pieceId];
const captures = this.board.piecesTilesCaptures[pieceId];
return (moves.length || captures.length) ? this.board.piecePositions[pieceId] : undefined;
}).filter((position) => position === null || position === void 0 ? void 0 : position.active);
const remaining = positions[Math.floor(Math.random() * positions.length)];
const { col, row } = remaining || { col: "E", row: "1" };
return { col, row };
}
}
}
class View {
constructor(element, game, perspective) {
this.element = element;
this.game = game;
this.setPerspective(perspective || this.game.turn);
this.tiles = Utils.getInitialBoardTiles(this.element, this.handleTileClick.bind(this));
this.pieces = Utils.getInitialBoardPieces(this.element, this.game.board.pieces);
this.drawPiecePositions();
}
drawActivePiece(activePieceId) {
const { row, col } = this.game.board.piecePositions[activePieceId];
this.tiles[row][col].classList.add("highlight-active");
this.pieces[activePieceId].classList.add("highlight-active");
}
drawCapturedPiece(capturedPieceId) {
const piece = this.pieces[capturedPieceId];
piece.style.setProperty("--transition-delay", "var(--transition-duration)");
piece.style.removeProperty("--pos-col");
piece.style.removeProperty("--pos-row");
piece.style.setProperty("--scale", "0");
}
drawPiecePositions(moves = [], moveInner = "") {
document.body.style.setProperty("--color-background", `var(--color-${this.game.turn.toLowerCase()}`);
const other = this.game.turn === "WHITE" ? "turn-black" : "turn-white";
const current = this.game.turn === "WHITE" ? "turn-white" : "turn-black";
this.element.classList.add(current);
this.element.classList.remove(other);
if (moves.length) {
this.element.classList.add("touching");
}
else {
this.element.classList.remove("touching");
}
const key = (row, col) => `${row}-${col}`;
const moveKeys = moves.map(({ row, col }) => key(row, col));
this.game.board.tileEach(({ row, col }, piece, pieceMoves, pieceCaptures) => {
const tileElement = this.tiles[row][col];
const move = moveKeys.includes(key(row, col)) ? moveInner : "";
const format = (id, className) => this.game.board.pieces[id].shape();
tileElement.innerHTML = `
<div class="move">${move}</div>
<div class="moves">
${this.game.board.tilesPiecesBlackMoves[row][col].map((id) => format(id, "black")).join("")}
${this.game.board.tilesPiecesWhiteMoves[row][col].map((id) => format(id, "white")).join("")}
</div>
<div class="captures">
${this.game.board.tilesPiecesBlackCaptures[row][col].map((id) => format(id, "black")).join("")}
${this.game.board.tilesPiecesWhiteCaptures[row][col].map((id) => format(id, "white")).join("")}
</div>
`;
if (piece) {
tileElement.classList.add("occupied");
const pieceElement = this.pieces[piece.data.id];
pieceElement.style.setProperty("--pos-col", Utils.colToInt(col).toString());
pieceElement.style.setProperty("--pos-row", Utils.rowToInt(row).toString());
pieceElement.style.setProperty("--scale", "1");
pieceElement.classList[(pieceMoves === null || pieceMoves === void 0 ? void 0 : pieceMoves.length) ? "add" : "remove"]("can-move");
pieceElement.classList[(pieceCaptures === null || pieceCaptures === void 0 ? void 0 : pieceCaptures.length) ? "add" : "remove"]("can-capture");
if (piece.updateShape) {
piece.updateShape = false;
pieceElement.innerHTML = piece.shape();
}
}
else {
tileElement.classList.remove("occupied");
}
});
}
drawPositions(moves, captures) {
moves === null || moves === void 0 ? void 0 : moves.forEach(({ row, col }) => {
var _a, _b;
this.tiles[row][col].classList.add("highlight-move");
(_b = this.pieces[(_a = this.game.board.tileFind({ row, col })) === null || _a === void 0 ? void 0 : _a.data.id]) === null || _b === void 0 ? void 0 : _b.classList.add("highlight-move");
});
captures === null || captures === void 0 ? void 0 : captures.forEach(({ row, col, capture }) => {
var _a, _b;
if (capture) {
row = capture.row;
col = capture.col;
}
this.tiles[row][col].classList.add("highlight-capture");
(_b = this.pieces[(_a = this.game.board.tileFind({ row, col })) === null || _a === void 0 ? void 0 : _a.data.id]) === null || _b === void 0 ? void 0 : _b.classList.add("highlight-capture");
});
}
drawResetClassNames() {
document.querySelectorAll(".highlight-active").forEach((element) => element.classList.remove("highlight-active"));
document.querySelectorAll(".highlight-capture").forEach((element) => element.classList.remove("highlight-capture"));
document.querySelectorAll(".highlight-move").forEach((element) => element.classList.remove("highlight-move"));
}
handleTileClick(location) {
const { activePieceId, capturedPieceId, moves = [], captures = [], type } = this.game.activate(location);
this.drawResetClassNames();
if (type === "TOUCH") {
const enPassant = captures.find((capture) => !!capture.capture);
const passingMoves = enPassant ? moves.concat([enPassant]) : moves;
this.drawPiecePositions(passingMoves, this.game.board.pieces[activePieceId].shape());
}
else {
this.drawPiecePositions();
}
if (type === "CANCEL" || type === "INVALID") {
return;
}
if (type === "MOVE" || type === "CAPTURE") {
}
else {
this.drawActivePiece(activePieceId);
}
if (type === "TOUCH") {
this.drawPositions(moves, captures);
}
else if (type === "CAPTURE") {
this.drawCapturedPiece(capturedPieceId);
}
// crazy town
// this.setPerspective(this.game.turn);
}
setPerspective(perspective) {
const other = perspective === "WHITE" ? "perspective-black" : "perspective-white";
const current = perspective === "WHITE" ? "perspective-white" : "perspective-black";
this.element.classList.add(current);
this.element.classList.remove(other);
}
}
class Control {
constructor(game, view) {
this.inputSpeedAsap = document.getElementById("speed-asap");
this.inputSpeedFast = document.getElementById("speed-fast");
this.inputSpeedMedium = document.getElementById("speed-medium");
this.inputSpeedSlow = document.getElementById("speed-slow");
this.inputRandomBlack = document.getElementById("black-random");
this.inputRandomWhite = document.getElementById("white-random");
this.inputPerspectiveBlack = document.getElementById("black-perspective");
this.inputPerspectiveWhite = document.getElementById("white-perspective");
this.game = game;
this.view = view;
this.inputPerspectiveBlack.addEventListener("change", this.updateViewPerspective.bind(this));
this.inputPerspectiveWhite.addEventListener("change", this.updateViewPerspective.bind(this));
this.updateViewPerspective();
}
get speed() {
if (this.inputSpeedAsap.checked) {
return 50;
}
if (this.inputSpeedFast.checked) {
return 250;
}
if (this.inputSpeedMedium.checked) {
return 500;
}
if (this.inputSpeedSlow.checked) {
return 1000;
}
}
autoplay() {
const input = this.game.turn === "WHITE" ? this.inputRandomWhite : this.inputRandomBlack;
if (!input.checked) {
setTimeout(this.autoplay.bind(this), this.speed);
return;
}
const position = this.game.randomMove();
this.view.handleTileClick(position);
setTimeout(this.autoplay.bind(this), this.speed);
}
updateViewPerspective() {
this.view.setPerspective(this.inputPerspectiveBlack.checked ? "BLACK" : "WHITE");
}
}
const DEMOS = {
castle1: "XD8B3,B1X,C1X,D1X,F1X,G1X",
castle2: "XD8B3,B1X,C1X,C2X,D1X,F1X,G1X",
castle3: "XD8E3,B1X,C1X,F2X,D1X,F1X,G1X",
promote1: "E1,E8,C2C7",
promote2: "E1,E8E7,PC2C8",
start: "XE7E6,F7F5,D2D4,E2E5",
test2: "C8E2,E8,G8H1,D7E4,H7H3,PA2H7,PB2G7,D2D6,E2E39,A1H2,E1B3",
test: "C8E2,E8,G8H1,D7E4,H7H3,D1H7,PB2G7,D2D6,E2E39,A1H2,E1B3",
};
const initialPositions = Utils.getInitialPiecePositions();
// const initialPositions = Utils.getPositionsFromShortCode(DEMOS.castle1);
const initialTurn = "WHITE";
const perspective = "WHITE";
const game = new Game(Utils.getInitialPieces(), initialPositions, initialTurn);
const view = new View(document.getElementById("board"), game, perspective);
const control = new Control(game, view);
control.autoplay();