ngx-chess-board
Version:
Chess game component
649 lines (578 loc) • 21.5 kB
text/typescript
import { EventEmitter } from '@angular/core';
import { HistoryMove } from '../history-move-provider/history-move';
import { ColorInput, PieceTypeInput } from '../utils/inputs/piece-type-input';
import { AbstractEngineFacade } from './abstract-engine-facade';
import { BoardLoader } from './board-state-provider/board-loader/board-loader';
import { BoardState } from './board-state-provider/board-state/board-state';
import { BoardStateProvider } from './board-state-provider/board-state/board-state-provider';
import { MoveStateProvider } from './board-state-provider/board-state/move-state-provider';
import { ClickUtils } from './click/click-utils';
import { Arrow } from './drawing-tools/shapes/arrow';
import { Circle } from './drawing-tools/shapes/circle';
import { DrawPoint } from './drawing-tools/draw-point';
import { DrawProvider } from './drawing-tools/draw-provider';
import { Board } from '../models/board';
import { Color } from '../models/pieces/color';
import { King } from '../models/pieces/king';
import { Pawn } from '../models/pieces/pawn';
import { Piece } from '../models/pieces/piece';
import { Point } from '../models/pieces/point';
import { DefaultPgnProcessor } from './pgn/default-pgn-processor';
import { AvailableMoveDecorator } from './piece-decorator/available-move-decorator';
import { PiecePromotionResolver } from '../piece-promotion/piece-promotion-resolver';
import { MoveUtils } from '../utils/move-utils';
import { MoveChange } from './outputs/move-change/move-change';
import { PieceFactory } from './utils/piece-factory';
export class EngineFacade extends AbstractEngineFacade {
_selected = false;
drawPoint: DrawPoint;
drawProvider: DrawProvider;
boardStateProvider: BoardStateProvider;
moveStateProvider: MoveStateProvider;
moveChange: EventEmitter<MoveChange>;
private historyMoveCandidate: HistoryMove;
constructor(
board: Board,
moveChange: EventEmitter<MoveChange>
) {
super(board);
this.moveChange = moveChange;
this.boardLoader = new BoardLoader(this);
this.boardLoader.addPieces();
this.boardStateProvider = new BoardStateProvider();
}
public reset(): void {
this.boardStateProvider.clear();
this.moveHistoryProvider.clear();
this.boardLoader.addPieces();
this.board.reset();
this.coords.reset();
this.drawProvider.clear();
this.pgnProcessor.reset();
}
public undo(): void {
if (!this.boardStateProvider.isEmpty()) {
const lastBoard = this.boardStateProvider.pop().board;
if (this.board.reverted) {
lastBoard.reverse();
}
this.board = lastBoard;
this.board.possibleCaptures = [];
this.board.possibleMoves = [];
this.board.activePiece = null;
this.moveHistoryProvider.pop();
this.board.calculateFEN();
this.pgnProcessor.removeLast();
}
}
saveMoveClone() {
const clone = this.board.clone();
if (this.board.reverted) {
clone.reverse();
}
this.moveStateProvider.addMove(new BoardState(clone));
}
public move(coords: string) {
if (coords) {
const sourceIndexes = MoveUtils.translateCoordsToIndex(
coords.substring(0, 2),
this.board.reverted
);
const destIndexes = MoveUtils.translateCoordsToIndex(
coords.substring(2, 4),
this.board.reverted
);
const srcPiece = this.board.getPieceByPoint(
sourceIndexes.yAxis,
sourceIndexes.xAxis
);
if (srcPiece) {
if (
(this.board.currentWhitePlayer &&
srcPiece.color === Color.BLACK) ||
(!this.board.currentWhitePlayer &&
srcPiece.color === Color.WHITE)
) {
return;
}
this.prepareActivePiece(srcPiece, srcPiece.point);
if (
this.board.isPointInPossibleMoves(
new Point(destIndexes.yAxis, destIndexes.xAxis)
) ||
this.board.isPointInPossibleCaptures(
new Point(destIndexes.yAxis, destIndexes.xAxis)
)
) {
this.saveClone();
this.movePiece(
srcPiece,
new Point(destIndexes.yAxis, destIndexes.xAxis),
coords.length === 5 ? +coords.substring(4, 5) : 0
);
this.board.lastMoveSrc = new Point(
sourceIndexes.yAxis,
sourceIndexes.xAxis
);
this.board.lastMoveDest = new Point(
destIndexes.yAxis,
destIndexes.xAxis
);
this.disableSelection();
} else {
this.disableSelection();
}
}
}
}
prepareActivePiece(pieceClicked: Piece, pointClicked: Point) {
this.board.activePiece = pieceClicked;
this._selected = true;
this.board.possibleCaptures = new AvailableMoveDecorator(
pieceClicked,
pointClicked,
this.board.currentWhitePlayer ? Color.WHITE : Color.BLACK,
this.board
).getPossibleCaptures();
this.board.possibleMoves = new AvailableMoveDecorator(
pieceClicked,
pointClicked,
this.board.currentWhitePlayer ? Color.WHITE : Color.BLACK,
this.board
).getPossibleMoves();
}
onPieceClicked(pieceClicked, pointClicked) {
if (
(this.board.currentWhitePlayer && pieceClicked.color === Color.BLACK) ||
(!this.board.currentWhitePlayer && pieceClicked.color === Color.WHITE)
) {
return;
}
this.prepareActivePiece(pieceClicked, pointClicked);
}
public handleClickEvent(pointClicked: Point, isMouseDown: boolean) {
let moving = false;
if (((
this.board.isPointInPossibleMoves(pointClicked) ||
this.board.isPointInPossibleCaptures(pointClicked)
) || this.freeMode) && pointClicked.isInRange()) {
this.saveClone();
this.board.lastMoveSrc = new Point(
this.board.activePiece.point.row,
this.board.activePiece.point.col
);
this.board.lastMoveDest = pointClicked.clone();
this.movePiece(this.board.activePiece, pointClicked);
if (!this.board.activePiece.point.isEqual(this.board.lastMoveSrc)) {
moving = true;
}
}
if (isMouseDown || moving) {
this.disableSelection();
}
this.disableSelection();
const pieceClicked = this.board.getPieceByPoint(
pointClicked.row,
pointClicked.col
);
if (pieceClicked && !moving) {
this.onFreeMode(pieceClicked);
this.onPieceClicked(pieceClicked, pointClicked);
}
}
onMouseDown(
event: MouseEvent,
pointClicked: Point,
left?: number,
top?: number
) {
this.moveDone = false;
if (event.button !== 0) {
this.drawPoint = ClickUtils.getDrawingPoint(
this.heightAndWidth,
this.colorStrategy,
event.x,
event.y,
event.ctrlKey,
event.altKey,
event.shiftKey,
left,
top
);
return;
}
this.drawProvider.clear();
if (
this.board.activePiece &&
pointClicked.isEqual(this.board.activePiece.point)
) {
this.disabling = true;
return;
}
const pieceClicked = this.board.getPieceByPoint(
pointClicked.row,
pointClicked.col
);
if (this.freeMode) {
if (pieceClicked) {
if (event.ctrlKey) {
this.board.pieces = this.board.pieces.filter(e => e !== pieceClicked);
return;
}
this.board.currentWhitePlayer = (pieceClicked.color === Color.WHITE);
}
}
if (this.isPieceDisabled(pieceClicked)) {
return;
}
if (this._selected) {
this.handleClickEvent(pointClicked, true);
} else {
if (pieceClicked) {
this.onFreeMode(pieceClicked);
this.onPieceClicked(pieceClicked, pointClicked);
}
}
}
onMouseUp(
event: MouseEvent,
pointClicked: Point,
left: number,
top: number
) {
this.moveDone = false;
if (event.button !== 0 && !this.drawDisabled) {
this.addDrawPoint(
event.x,
event.y,
event.ctrlKey,
event.altKey,
event.shiftKey,
left, top
);
return;
}
this.drawProvider.clear();
if (this.dragDisabled) {
return;
}
if (
this.board.activePiece &&
pointClicked.isEqual(this.board.activePiece.point) &&
this.disabling
) {
this.disableSelection();
this.disabling = false;
return;
}
const pieceClicked = this.board.getPieceByPoint(
pointClicked.row,
pointClicked.col
);
if (this.isPieceDisabled(pieceClicked)) {
return;
}
if (this._selected) {
this.handleClickEvent(pointClicked, false);
// this.possibleMoves = activePiece.getPossibleMoves();
}
}
saveClone() {
const clone = this.board.clone();
if (this.board.reverted) {
clone.reverse();
}
this.boardStateProvider.addMove(new BoardState(clone));
}
movePiece(toMovePiece: Piece, newPoint: Point, promotionIndex?: number) {
const destPiece = this.board.pieces.find(
(piece) =>
piece.point.col === newPoint.col &&
piece.point.row === newPoint.row
);
this.pgnProcessor.process(
this.board,
toMovePiece,
newPoint,
destPiece
);
if (destPiece && toMovePiece.color !== destPiece.color) {
this.board.pieces = this.board.pieces.filter(
(piece) => piece !== destPiece
);
} else {
if (destPiece && toMovePiece.color === destPiece.color) {
return;
}
}
this.historyMoveCandidate = new HistoryMove(
MoveUtils.format(toMovePiece.point, newPoint, this.board.reverted),
toMovePiece.constant.name,
toMovePiece.color === Color.WHITE ? 'white' : 'black',
!!destPiece
);
this.moveHistoryProvider.addMove(this.historyMoveCandidate);
if (toMovePiece instanceof King) {
const squaresMoved = Math.abs(newPoint.col - toMovePiece.point.col);
if (squaresMoved > 1) {
if (newPoint.col < 3) {
const leftRook = this.board.getPieceByField(
toMovePiece.point.row,
0
);
if (!this.freeMode) {
leftRook.point.col = this.board.reverted ? 2 : 3;
}
} else {
const rightRook = this.board.getPieceByField(
toMovePiece.point.row,
7
);
if (!this.freeMode) {
rightRook.point.col = this.board.reverted ? 4 : 5;
}
}
}
}
if (toMovePiece instanceof Pawn) {
this.board.checkIfPawnTakesEnPassant(newPoint);
this.board.checkIfPawnEnpassanted(toMovePiece, newPoint);
} else {
this.board.enPassantPoint = null;
this.board.enPassantPiece = null;
}
toMovePiece.point = newPoint;
this.increaseFullMoveCount();
this.board.currentWhitePlayer = !this.board.currentWhitePlayer;
if (!this.checkForPawnPromote(toMovePiece, promotionIndex)) {
this.afterMoveActions();
}
}
checkForPawnPromote(toPromotePiece: Piece, promotionIndex?: number) {
if (!(toPromotePiece instanceof Pawn)) {
return;
}
if (toPromotePiece.point.row === 0 || toPromotePiece.point.row === 7) {
this.board.pieces = this.board.pieces.filter(
(piece) => piece !== toPromotePiece
);
// When we make move manually, we pass promotion index already, so we don't need
// to acquire it from promote dialog
if (!promotionIndex) {
this.openPromoteDialog(toPromotePiece);
} else {
PiecePromotionResolver.resolvePromotionChoice(
this.board,
toPromotePiece,
promotionIndex
);
this.afterMoveActions(promotionIndex);
}
return true;
}
}
afterMoveActions(promotionIndex?: number) {
this.checkIfPawnFirstMove(this.board.activePiece);
this.checkIfRookMoved(this.board.activePiece);
this.checkIfKingMoved(this.board.activePiece);
this.board.blackKingChecked = this.board.isKingInCheck(
Color.BLACK,
this.board.pieces
);
this.board.whiteKingChecked = this.board.isKingInCheck(
Color.WHITE,
this.board.pieces
);
const check =
this.board.blackKingChecked || this.board.whiteKingChecked;
const checkmate =
this.checkForPossibleMoves(Color.BLACK) ||
this.checkForPossibleMoves(Color.WHITE);
const stalemate =
this.checkForPat(Color.BLACK) || this.checkForPat(Color.WHITE);
this.historyMoveCandidate.setGameStates(check, stalemate, checkmate);
this.pgnProcessor.processChecks(checkmate, check, stalemate);
this.pgnProcessor.addPromotionChoice(promotionIndex);
this.disabling = false;
this.board.calculateFEN();
const lastMove = this.moveHistoryProvider.getLastMove();
if (lastMove && promotionIndex) {
lastMove.move += promotionIndex;
}
this.moveChange.emit({
...lastMove,
check,
checkmate,
stalemate,
fen: this.board.fen,
pgn: {
pgn: this.pgnProcessor.getPGN()
},
freeMode: this.freeMode
});
this.moveDone = true;
}
checkForPat(color: Color) {
if (color === Color.WHITE && !this.board.whiteKingChecked) {
return this.checkForPossibleMoves(color);
} else {
if (color === Color.BLACK && !this.board.blackKingChecked) {
return this.checkForPossibleMoves(color);
}
}
}
openPromoteDialog(piece: Piece) {
if (piece.color === this.board.activePiece.color) {
this.modal.open((index) => {
PiecePromotionResolver.resolvePromotionChoice(
this.board,
piece,
index
);
this.afterMoveActions(index);
});
}
}
checkForPossibleMoves(color: Color): boolean {
return !this.board.pieces
.filter((piece) => piece.color === color)
.some(
(piece) =>
piece
.getPossibleMoves()
.some(
(move) =>
!MoveUtils.willMoveCauseCheck(
color,
piece.point.row,
piece.point.col,
move.row,
move.col,
this.board
)
) ||
piece
.getPossibleCaptures()
.some(
(capture) =>
!MoveUtils.willMoveCauseCheck(
color,
piece.point.row,
piece.point.col,
capture.row,
capture.col,
this.board
)
)
);
}
disableSelection() {
this._selected = false;
this.board.possibleCaptures = [];
this.board.activePiece = null;
this.board.possibleMoves = [];
}
/**
* Processes logic to allow freeMode based logic processing
*/
onFreeMode(pieceClicked) {
if (
!this.freeMode ||
pieceClicked === undefined ||
pieceClicked === null
) {
return;
}
// sets player as white in-case white pieces are selected, and vice-versa when black is selected
this.board.currentWhitePlayer = pieceClicked.color === Color.WHITE;
}
isPieceDisabled(pieceClicked: Piece) {
if (pieceClicked && pieceClicked.point) {
const foundCapture = this.board.possibleCaptures.find(
(capture) =>
capture.col === pieceClicked.point.col &&
capture.row === pieceClicked.point.row
);
if (foundCapture) {
return false;
}
}
return (
pieceClicked &&
((this.lightDisabled && pieceClicked.color === Color.WHITE) ||
(this.darkDisabled && pieceClicked.color === Color.BLACK))
);
}
addDrawPoint(
x: number,
y: number,
crtl: boolean,
alt: boolean,
shift: boolean,
left: number,
top: number
) {
const upPoint = ClickUtils.getDrawingPoint(
this.heightAndWidth,
this.colorStrategy,
x,
y,
crtl,
alt,
shift,
left,
top
);
if (this.drawPoint.isEqual(upPoint)) {
const circle = new Circle();
circle.drawPoint = upPoint;
if (!this.drawProvider.containsCircle(circle)) {
this.drawProvider.addCircle(circle);
} else {
this.drawProvider.reomveCircle(circle);
}
} else {
const arrow = new Arrow();
arrow.start = this.drawPoint;
arrow.end = upPoint;
if (!this.drawProvider.containsArrow(arrow)) {
this.drawProvider.addArrow(arrow);
} else {
this.drawProvider.removeArrow(arrow);
}
}
}
increaseFullMoveCount() {
if (!this.board.currentWhitePlayer) {
++this.board.fullMoveCount;
}
}
addPiece(
pieceTypeInput: PieceTypeInput,
colorInput: ColorInput,
coords: string
) {
if (this.freeMode && coords && pieceTypeInput > 0 && colorInput > 0) {
let indexes = MoveUtils.translateCoordsToIndex(
coords,
this.board.reverted
);
let existing = this.board.getPieceByPoint(
indexes.yAxis,
indexes.xAxis
);
if (existing) {
this.board.pieces = this.board.pieces.filter(e => e !== existing);
}
let createdPiece = PieceFactory.create(
indexes,
pieceTypeInput,
colorInput,
this.board
);
this.saveClone();
this.board.pieces.push(createdPiece);
this.afterMoveActions();
}
}
}