onix-chess
Version:
Onix chess library
1,394 lines (1,241 loc) • 67.3 kB
text/typescript
import cloneDeep from 'lodash/cloneDeep';
import indexOf from 'lodash/indexOf';
import { Colors, Pieces, Squares, Directions } from '../types/Types';
import { Color } from './Color';
import { Castling, CastlingSide } from './Castling';
import { Direction } from './Direction';
import { Piece } from './Piece';
import { Square } from './Square';
import { SimpleMove } from './SimpleMove';
import { FenString } from './FenString';
/** Short aliases */
const ns = Square.NullSquare;
const noPiece = Piece.None;
const {
Null: NULL_DIR, Up: UP, Down: DOWN, Left: LEFT, Right: RIGHT,
UpLeft: UP_LEFT, UpRight: UP_RIGHT, DownLeft: DOWN_LEFT, DownRight: DOWN_RIGHT } = Direction;
function is_valid_dest(dest: Squares.Square, sqset?: Squares.Square[]) {
return ((sqset === undefined) || indexOf(sqset, dest) !== -1);
}
export enum SanCheckLevel
{
NoCheckTest = 0,
CheckText = 1,
MateTest = 2,
}
export enum GenerateMode
{
Captures = 1,
NonCaptures = 2,
All = 3
}
/**
* Chess position
*/
export class Position {
private brd: (Pieces.Piece | Pieces.Empty)[] = [];
private capt: number[] = [];
private plyCnt: number = 0;
private strictCastling: boolean = false;
private pinned: Directions.Direction[] = [];
private list: Squares.Square[][] = []; // list of piece squares for each side
private listPos: number[] = []; // ListPos stores the position in list[][] for the piece on square x.
private pieceCount: number[] = [];
private material: number[] = [];
private numOnRank: number[][] = [];
private numOnFyle: number[][] = [];
private numOnLeftDiag: number[][] = [];
private numOnRightDiag: number[][] = [];
private numOnSquareColor: number[][] = [];
private wm: Colors.BW = Color.White;
private castling: Castling = new Castling();
public EpTarget?: Squares.Square = ns;
public HalfMoveCount: number = 0;
/**
* @constructor
*/
constructor(fen?: string) {
this.clear();
if (fen) {
FenString.toPosition(this, fen);
}
}
public clear(): void {
this.wm = Color.White;
this.capt = [];
this.EpTarget = ns;
this.castling.clear();
this.HalfMoveCount = 0;
this.plyCnt = 0;
this.pieceCount[Color.White] = 0;
this.pieceCount[Color.Black] = 0;
this.list[Color.White] = [];
this.list[Color.Black] = [];
this.listPos = [];
let i: number;
let j: number;
for (i = 0; i <= 64; i++) {
this.brd[i] = noPiece;
}
for (i = Piece.WKing; i <= Piece.BPawn; i++) {
this.material[i] = 0;
this.numOnRank[i] = [];
this.numOnFyle[i] = [];
this.numOnLeftDiag[i] = [];
this.numOnRightDiag[i] = [];
this.numOnSquareColor[i] = [];
for (j = 0; j < 8; j++) {
this.numOnRank[i][j] = 0;
this.numOnFyle[i][j] = 0;
}
for (j = 0; j < 15; j++) {
this.numOnLeftDiag[i][j] = 0;
this.numOnRightDiag[i][j] = 0;
}
this.numOnSquareColor[i][Color.White] = 0;
this.numOnSquareColor[i][Color.Black] = 0;
}
}
/**
* Copy position
* @param source {Position}
*/
public copyFrom(source: Position) {
this.brd = cloneDeep(source.brd);
this.pieceCount = cloneDeep(source.pieceCount);
this.listPos = cloneDeep(source.listPos);
this.list = cloneDeep(source.list);
this.material = cloneDeep(source.material);
this.numOnFyle = cloneDeep(source.numOnFyle);
this.numOnRank = cloneDeep(source.numOnRank);
this.numOnLeftDiag = cloneDeep(source.numOnLeftDiag);
this.numOnRightDiag = cloneDeep(source.numOnRightDiag);
this.numOnSquareColor = cloneDeep(source.numOnSquareColor);
this.numOnSquareColor = cloneDeep(source.numOnSquareColor);
this.EpTarget = source.EpTarget;
this.wm = source.wm;
this.plyCnt = source.plyCnt;
this.HalfMoveCount = source.HalfMoveCount;
this.castling = source.castling.clone();
}
/**
* Clone position object
* @returns ChessPosition
*/
public clone() {
const sp = new Position();
sp.copyFrom(this);
return sp;
}
public get WhoMove(): Colors.BW {
return this.wm;
}
public set WhoMove(value: Colors.BW) {
this.wm = value;
}
/**
* Byte board
*/
public get Board(): (Pieces.Piece | Pieces.Empty)[] {
return this.brd;
}
public get Captured(): number[] {
return this.capt;
}
/**
* Castling
*/
public get Castling(): Castling {
return this.castling;
}
/**
* Get piece on square
*/
getPiece = (sq: number): (Pieces.Piece | Pieces.Empty) => {
return this.brd[sq];
}
/**
*
*/
getPieceNum = (sq: number): number => {
return this.listPos[sq];
}
/**
* Return true if count of specified piece great than zero.
*/
public hasPiece(p: number) {
return this.material[p] > 0;
}
/**
* Return count of specified piece.
*/
public getPieceCount(p: number) {
return this.material[p];
}
/**
* Add piece to square
* @param p
* @param sq
* @returns Boolean
*/
public addPiece(p: Pieces.Piece, sq: Squares.Square): boolean {
const c = Piece.color(p);
if (this.pieceCount[c] === 16) { return false; }
if (Piece.type(p) === Piece.King) {
// check there is not already a King:
if (this.material[p] > 0) { return false; }
// king is always at the start of the piece list, so move the piece
// already at location 0 if there is one:
if (this.pieceCount[c] > 0) {
const oldsq = this.list[c][0];
this.list[c][this.pieceCount[c]] = oldsq;
this.listPos[oldsq] = this.pieceCount[c];
}
this.list[c][0] = sq;
this.listPos[sq] = 0;
} else {
this.listPos[sq] = this.pieceCount[c];
this.list[c][this.pieceCount[c]] = sq;
}
this.pieceCount[c]++;
this.material[p]++;
this.addToBoard(p, sq);
return true;
}
/**
* Remove piece from square
* @param p
* @param sq
* @returns Boolean
*/
public removePiece(p: Pieces.Piece, sq: Squares.Square): boolean {
// TODO: check method
const c = Piece.color(p);
if (this.pieceCount[c] === 0) { return false; }
if (this.material[p] === 0) { return false; }
this.removeFromBoard(p, sq);
this.material[p]--;
this.pieceCount[c]--;
if (Piece.type(p) === Piece.King) {
this.list[c][0] = 0;
this.listPos[sq] = 0;
} else {
this.listPos[sq] = this.pieceCount[c];
this.list[c][this.pieceCount[c]] = sq;
}
return true;
}
/**
* Return current halfmove humber
*/
public get PlyCount(): number {
return this.plyCnt;
}
/**
* Return current halfmove humber
*/
public set PlyCount(val: number) {
this.plyCnt = val;
}
/**
* Make the move on the board and update all the necessary
* fields in the simpleMove structure so it can be undone.
* @param sm {SimpleMove}
*/
public doSimpleMove(sm: SimpleMove): void {
const from = Square.notEmpty(sm.from);
const to = Square.notEmpty(sm.to);
let piece = Piece.notEmpty(this.brd[from]);
const ptype = Piece.type(piece);
const enemy = Color.flip(this.wm);
sm.pieceNum = this.listPos[from];
sm.capturedPiece = this.brd[to];
sm.capturedSquare = to;
sm.castleFlags = this.castling.Flag;
sm.epSquare = this.EpTarget;
sm.oldHalfMoveClock = this.HalfMoveCount;
this.HalfMoveCount++;
this.plyCnt++;
// handle enpassant capture:
if ((ptype === Piece.Pawn) &&
(sm.capturedPiece === noPiece) &&
(Square.fyle(from) !== Square.fyle(to))) {
// this was an EP capture. We do not need to check it was a capture
// since if a pawn lands on EPTarget it must capture to get there.
const enemyPawn = Piece.create(enemy, Piece.Pawn);
sm.capturedSquare = (this.wm === Color.White ? (to - 8) as Squares.Square : (to + 8)) as Squares.Square;
sm.capturedPiece = enemyPawn;
}
// handle captures:
if (Piece.isPiece(sm.capturedPiece) && Square.isSquare(sm.capturedSquare)) {
sm.capturedNum = this.listPos[sm.capturedSquare];
// update opponents List of pieces
this.pieceCount[enemy]--;
this.listPos[this.list[enemy][this.pieceCount[enemy]]] = sm.capturedNum;
this.list[enemy][sm.capturedNum] = this.list[enemy][this.pieceCount[enemy]];
this.material[sm.capturedPiece]--;
this.HalfMoveCount = 0;
this.removeFromBoard(sm.capturedPiece, sm.capturedSquare);
this.capt[this.plyCnt] = sm.capturedPiece;
}
// handle promotion:
if (sm.promote !== noPiece) {
this.material[piece]--;
this.removeFromBoard(piece, from);
piece = Piece.create(this.wm, sm.promote);
this.material[piece]++;
this.addToBoard(piece, from);
}
// now make the move:
this.list[this.wm][sm.pieceNum] = to;
this.listPos[to] = sm.pieceNum;
this.removeFromBoard(piece, from);
this.addToBoard(piece, to);
// handle Castling:
if ((ptype === Piece.King) &&
(Square.fyle(from) === 4) &&
(Square.fyle(to) === 2 || Square.fyle(to) === 6)) {
let rook = Piece.create(this.wm, Piece.Rook);
let rookfrom: Squares.Square;
let rookto: Squares.Square;
if (Square.fyle(to) === 2) {
rookfrom = (to - 2) as Squares.Square;
rookto = (to + 1) as Squares.Square;
} else {
rookfrom = (to + 1) as Squares.Square;
rookto = (to - 1) as Squares.Square;
}
this.listPos[rookto] = this.listPos[rookfrom];
this.list[this.wm][this.listPos[rookto]] = rookto;
this.removeFromBoard(rook, rookfrom);
this.addToBoard(rook, rookto);
}
// handle clearing of castling flags:
if (this.castling) {
if (ptype === Piece.King) { // the king moved.
this.castling.set(this.wm, CastlingSide.Queen, false);
this.castling.set(this.wm, CastlingSide.King, false);
}
// see if a rook moved or was captured:
if (this.wm === Color.White) {
if (from === 0) { this.castling.set(Color.White, CastlingSide.Queen, false); }
if (from === 7) { this.castling.set(Color.White, CastlingSide.King, false); }
if (to === 56) { this.castling.set(Color.Black, CastlingSide.Queen, false); }
if (to === 63) { this.castling.set(Color.Black, CastlingSide.King, false); }
} else {
if (from === 56) { this.castling.set(Color.Black, CastlingSide.Queen, false); }
if (from === 63) { this.castling.set(Color.Black, CastlingSide.King, false); }
if (to === 0) { this.castling.set(Color.White, CastlingSide.Queen, false); }
if (to === 7) { this.castling.set(Color.White, CastlingSide.King, false); }
}
}
// set the EPTarget square, if a pawn advanced two squares and an
// enemy pawn is on a square where enpassant may be possible.
this.EpTarget = ns;
if (ptype === Piece.Pawn) {
const fromRank = Square.rank(from);
const toRank = Square.rank(to);
const epLeft = Square.move(to, LEFT);
const epRight = Square.move(to, LEFT);
if ((fromRank === 1) &&
(toRank === 3) &&
(
(Square.isSquare(epLeft) && (this.brd[epLeft] === Piece.BPawn)) ||
(Square.isSquare(epRight) && (this.brd[epRight] === Piece.BPawn)))) {
this.EpTarget = Square.move(from, UP);
}
if ((fromRank === 6) &&
(toRank === 4) &&
(
(Square.isSquare(epLeft) && (this.brd[epLeft] === Piece.WPawn)) ||
(Square.isSquare(epRight) && (this.brd[epRight] === Piece.WPawn)))) {
this.EpTarget = Square.move(from, DOWN);
}
this.HalfMoveCount = 0; // 50-move clock resets on pawn moves.
}
this.wm = enemy;
return;
}
/**
* Take back a simple move that has been made with DoSimpleMove().
* @param sm {SimpleMove}
*/
public undoSimpleMove(sm: SimpleMove) {
const from = Square.notEmpty(sm.from);
const to = Square.notEmpty(sm.to);
let piece = Piece.notEmpty(this.brd[to]);
this.EpTarget = sm.epSquare;
this.castling.Flag = sm.castleFlags;
this.HalfMoveCount = sm.oldHalfMoveClock;
this.plyCnt--;
this.wm = Color.flip(this.wm);
sm.pieceNum = this.listPos[to];
// handle a capture: insert piece back into piece list.
// this works for EP captures too, since the square of the captured
// piece is in the "capturedSquare" field rather than assuming the
// value of the "to" field. The only time these two fields are
// different is for an enpassant move.
if (Piece.isPiece(sm.capturedPiece) && Square.isSquare(sm.capturedSquare)) {
const c = Color.flip(this.wm);
this.listPos[this.list[c][sm.capturedNum]] = this.pieceCount[c];
this.listPos[sm.capturedSquare] = sm.capturedNum;
this.list[c][this.pieceCount[c]] = this.list[c][sm.capturedNum];
this.list[c][sm.capturedNum] = sm.capturedSquare;
this.material[sm.capturedPiece]++;
this.pieceCount[c]++;
}
// handle promotion:
if (sm.promote !== noPiece) {
this.material[piece]--;
this.removeFromBoard(piece, to);
piece = Piece.create(this.wm, Piece.Pawn);
this.material[piece]++;
this.addToBoard(piece, to);
}
// now make the move:
this.list[this.wm][sm.pieceNum] = from;
this.listPos[from] = sm.pieceNum;
this.removeFromBoard(piece, to);
this.addToBoard(piece, from);
if (Piece.isPiece(sm.capturedPiece) && Square.isSquare(sm.capturedSquare)) {
this.addToBoard(sm.capturedPiece, sm.capturedSquare);
delete this.capt[this.plyCnt + 1];
}
// handle Castling:
if ((Piece.type(piece) === Piece.King) && Square.fyle(from) === 4
&& (Square.fyle(to) === 2 || Square.fyle(to) === 6)) {
const rook = (this.wm === Color.White ? Piece.WRook : Piece.BRook);
let rookfrom: Squares.Square;
let rookto: Squares.Square;
if (Square.fyle(to) === 2) {
rookfrom = (to - 2) as Squares.Square;
rookto = (to + 1) as Squares.Square;
} else {
rookfrom = (to + 1) as Squares.Square;
rookto = (to - 1) as Squares.Square;
}
this.listPos[rookfrom] = this.listPos[rookto];
this.list[this.wm][this.listPos[rookto]] = rookfrom;
this.removeFromBoard(rook, rookto);
this.addToBoard(rook, rookfrom);
}
}
public makeSanString(sm: SimpleMove, flag: SanCheckLevel = SanCheckLevel.MateTest) {
let san = "";
const sFrom = Square.notEmpty(sm.from);
sm.pieceNum = this.listPos[sFrom];
const from = this.list[this.wm][sm.pieceNum];
const piece = Piece.notEmpty(this.brd[from]);
const p = Piece.type(piece);
const to = Square.notEmpty(sm.to);
let mlist: SimpleMove[];
if (p === Piece.Pawn) {
if (Square.fyle(from) !== Square.fyle(to)) { // pawn capture
san += Square.fyleChar(from);
san += "x";
}
san += Square.fyleChar(to);
san += Square.rankChar(to);
if (((Square.rank(to) === 0) || (Square.rank(to) === 7)) && (sm.promote !== noPiece)) {
san += "=";
san += Piece.toChar(sm.promote);
}
} else if (p === Piece.King) {
if (sm.from === ns) {
san += "--";
} else if ((Square.fyle(from) === 4) && (Square.fyle(to) === 6)) {
san += Castling.K;
} else if ((Square.fyle(from) === 4) && (Square.fyle(to) === 2)) {
san += Castling.Q;
} else { // regular King move
san += "K";
if (this.brd[to] !== noPiece) {
san += "x";
}
san += Square.fyleChar(to);
san += Square.rankChar(to);
}
} else { // is Queen/Rook/Bishop/Knight
san += Piece.toChar(p);
// we only need to calculate legal moves to disambiguate if there
// are more than one of this type of piece.
if (this.material[Piece.notEmpty(this.brd[sFrom])] < 2) {
if (this.brd[to] !== noPiece) {
san += "x";
}
san += Square.fyleChar(to);
san += Square.rankChar(to);
} else {
let ambiguity: number = 0;
const f = Square.fyleChar(from);
const r = Square.rankChar(from);
mlist = [];
this.matchLegalMove(mlist, p, to);
for (let i = 0; i < mlist.length; i++) {
const m2 = mlist[i];
const from2 = Square.notEmpty(m2.from);
const p2 = Piece.type(Piece.notEmpty(this.brd[from2]));
if ((to === m2.to) && (from !== from2) && (p2 === p)) {
/* we have an ambiguity */
const f2 = Square.fyleChar(from2);
const r2 = Square.rankChar(from2);
// Ambiguity:
// 1 (0001) --> need from-file (preferred) or from-rank
// 3 (0011) --> need from-file
// 5 (0101) --> need from-rank
// 7 (0111) --> need both from-file and from-rank
ambiguity |= 1;
if (r === r2) {
ambiguity |= 2; // 0b0010
} else if (f === f2) {
ambiguity |= 4; // 0b0100
}
}
}
if (ambiguity) {
if (ambiguity != 5) {
san += f; // print from-fyle
}
if (ambiguity >= 5) {
san += r; // print from-rank
}
}
if (this.brd[to] !== noPiece) {
san += "x";
}
san += Square.fyleChar(to);
san += Square.rankChar(to);
}
}
// now do the check or mate symbol:
if (flag > 0) {
// now we make the move to test for check:
this.doSimpleMove(sm);
if (this.isKingInCheck()) {
let ch = "+";
if (flag > 1) {
mlist = this.generateMoves();
if (mlist.length === 0) {
ch = "#";
}
}
san += ch;
}
this.undoSimpleMove(sm);
}
return san;
}
/**
* Generate the legal moves list.
* If the specified pieceType is not NOPIECE, then only legal moves for that type of piece
* are generated.
*/
public generateMoves(pieceType?: Pieces.PieceType, genType: number = GenerateMode.All, maybeInCheck: boolean = true) {
const genNonCaptures = (genType & GenerateMode.NonCaptures) !== 0;
const capturesOnly = !genNonCaptures;
let mask = 0;
if (pieceType !== noPiece) {
mask = 1 << pieceType;
} else {
mask = (1 << Piece.King) |
(1 << Piece.Queen) |
(1 << Piece.Rook) |
(1 << Piece.Bishop) |
(1 << Piece.Knight) |
(1 << Piece.Pawn);
}
// use the objects own move list if none was provided:
const mlist: SimpleMove[] = [];
// compute which pieces of the side to move are this.pinned to the king:
this.calcPins();
// determine if the side to move is in check and find where the
// checking pieces are, unless the caller has passed maybeInCheck=false
// indicating it is CERTAIN the side to move is not in check here.
let numChecks = 0;
if (maybeInCheck) {
const checkSquares: Squares.Square[] = [];
numChecks = this.calcNumChecks(this.getKingSquare(this.wm), checkSquares);
if (numChecks > 0) {
// the side to move IS in check:
this.genCheckEvasions(mlist, pieceType, genType, checkSquares);
return mlist;
}
}
// the side to move is NOT in check. Iterate over each non-king
// piece, and then generate King moves last of all:
const npieces = this.pieceCount[this.wm];
for (let x = 1; x < npieces; x++) {
const sq = this.list[this.wm][x];
const p = Piece.notEmpty(this.brd[sq]);
const ptype = Piece.type(p);
if (!(mask & (1 << ptype))) { continue; }
const _pinned = this.pinned[x];
// if this.pinned[x] == dir (not NULL_DIR), x can ONLY move along
// that direction or its opposite.
if (_pinned !== NULL_DIR) { // piece x is this.pinned to king
if (ptype === Piece.Pawn) {
this.genPawnMoves(mlist, sq, _pinned, genType);
} else if (ptype === Piece.Knight) {
// do nothing -- this.pinned knights cannot move
} else {
// piece is a this.pinned Queen/Rook/Bishop. Only generate
// moves along the this.pinned direction and its opposite:
if ((ptype === Piece.Queen) ||
((ptype === Piece.Rook) && !Direction.isDiagonal(_pinned)) ||
((ptype === Piece.Bishop) && Direction.isDiagonal(_pinned))) {
this.genSliderMoves(mlist, this.wm, sq, _pinned, capturesOnly);
this.genSliderMoves(mlist, this.wm, sq, Direction.opposite(_pinned), capturesOnly);
}
}
} else {
// this piece is free to move anywhere
if (ptype === Piece.Pawn) {
this.genPawnMoves(mlist, sq, NULL_DIR, genType);
} else {
// is Knight/Queen/Bishop/Rook:
this.genPieceMoves(mlist, sq, capturesOnly);
}
}
}
// lastly, king moves...
if (mask & (1 << Piece.King)) {
const castling = !numChecks;
this.genKingMoves(mlist, genType, castling);
}
return mlist;
}
public isLegalMove(sm: SimpleMove): boolean {
const from = sm.from;
const to = sm.to;
if (!Square.isSquare(from) || !Square.isSquare(to)) { return false; }
if (from === to) { return false; }
let mover = this.brd[from];
const captured = this.brd[to];
if (!Piece.isPiece(mover)) { return false; }
if (Piece.color(mover) !== this.wm) { return false; }
if (Piece.isPiece(captured) && Piece.color(captured) === this.wm) { return false; }
if (sm.movingPiece !== mover) { return false; }
mover = Piece.type(mover);
if (sm.promote !== noPiece && mover !== Piece.Pawn) { return false; }
const enemy = Color.flip(this.wm);
if (mover === Piece.Pawn) {
let rfrom = Square.rank(from);
let rto = Square.rank(to);
if (this.wm === Color.Black) {
rfrom = (7 - rfrom) as Squares.Rank;
rto = (7 - rto) as Squares.Rank;
}
const rdiff = rto - rfrom;
const fdiff = Square.fyle(to) - Square.fyle(from);
if (rdiff < 1 || rdiff > 2) { return false; }
if (fdiff < -1 || fdiff > 1) { return false; }
if (fdiff === 0) { // pawn push:
if (captured !== noPiece) { return false; }
if (rdiff === 2) { // two-square push:
if (rfrom !== 1) { return false; }
// make sure the square in between is NOPIECE:
const midsquare = from + ((to - from) / 2);
if (this.brd[midsquare] !== noPiece) { return false; }
}
} else { // pawn capture:
if (rdiff !== 1) { return false; }
if (captured === noPiece) {
// it must be enpassant, or illegal
if (to !== this.EpTarget) { return false; }
}
}
// check the promotion piece:
if (rto === 7) {
const p = sm.promote;
if (p !== Piece.Queen && p !== Piece.Rook && p !== Piece.Bishop && p !== Piece.Knight) {
return false;
}
} else {
if (sm.promote !== noPiece) { return false; }
}
} else if (Piece.isSlider(mover)) {
// make sure the direction is valid:
const dir = Square.direction(from, to);
if (dir === NULL_DIR) { return false; }
if (mover === Piece.Rook && Direction.isDiagonal(dir)) { return false; }
if (mover === Piece.Bishop && !Direction.isDiagonal(dir)) { return false; }
const delta = Direction.delta(dir);
// make sure all the in-between squares are NOPIECE:
let dest = from + delta;
while (dest !== to) {
if (this.brd[dest] !== noPiece) { return false; }
dest += delta;
}
} else if (mover === Piece.Knight) {
if (!Square.isKnightHop(from, to)) { return false; }
} else /* (mover == KING) */ {
if (Square.adjacent(to, this.getKingSquare(enemy))) { return false; }
if (!Square.adjacent(from, to)) {
// the move must be castling, or illegal.
if (this.isKingInCheck()) { return false; }
const mlist: SimpleMove[] = [];
this.genCastling(mlist);
for (let i = 0; i < mlist.length; i++) {
if ((mlist[i].from === from) && (mlist[i].to === to)) {
return false;
}
}
return false;
}
}
// the move looks good, but does it leave the king in check?
const kingSq = (mover === Piece.King) ? to : this.getKingSquare(this.wm);
this.doSimpleMove(sm);
const nchecks = this.calcAttacks(enemy, kingSq);
this.undoSimpleMove(sm);
return (nchecks === 0);
}
/**
* Добавить фигуру на доску
* @param p
* @param sq
*/
private addToBoard(p: Pieces.Piece, sq: Squares.Square): void {
this.brd[sq] = p;
this.numOnRank[p][Square.rank(sq)]++;
this.numOnFyle[p][Square.fyle(sq)]++;
this.numOnLeftDiag[p][Square.leftDiag(sq)]++;
this.numOnRightDiag[p][Square.rightDiag(sq)]++;
this.numOnSquareColor[p][Square.color(sq)]++;
}
/**
* Убрать фигуру с доски
* @param p
* @param sq
*/
private removeFromBoard(p: Pieces.Piece, sq: Squares.Square): void {
this.brd[sq] = noPiece;
this.numOnRank[p][Square.rank(sq)]--;
this.numOnFyle[p][Square.fyle(sq)]--;
this.numOnLeftDiag[p][Square.leftDiag(sq)]--;
this.numOnRightDiag[p][Square.rightDiag(sq)]--;
this.numOnSquareColor[p][Square.color(sq)]--;
}
private fyleCount(p: Pieces.Piece, f: number): number {
return this.numOnFyle[p][f];
}
private rankCount(p: Pieces.Piece, r: number): number {
return this.numOnRank[p][r];
}
private leftDiagCount(p: Pieces.Piece, diag: number): number {
return this.numOnLeftDiag[p][diag];
}
private rightDiagCount(p: Pieces.Piece, diag: number): number {
return this.numOnRightDiag[p][diag];
}
private squareColorCount(p: Pieces.Piece, sqColor: number): number {
return this.numOnSquareColor[p][sqColor];
}
/**
* Клетка на которой находится король
* @param c
* @returns Number
*/
private getKingSquare(c: Colors.BW = this.wm): Squares.Square {
return this.list[c][0];
}
/**
* Клетка на которой находится король противника
* @param c
* @returns Number
*/
private getEnemyKingSquare(c: Colors.BW = this.wm): Squares.Square {
return this.list[1 - c][0];
}
/**
* Посчитать количество шахов
*/
private calcNumChecks(kingSq: Squares.Square, checkSquares?: Squares.Square[]): number {
kingSq = (kingSq) ? kingSq : this.getKingSquare();
return this.calcAttacks(Color.flip(this.wm), kingSq, checkSquares);
}
/**
* Король под шахом
*/
public isKingInCheck(): boolean {
return (this.calcNumChecks(this.getKingSquare()) > 0);
}
/**
* Рассчитать количество атак для указанной стороны.
* Эта функция также добавляет список атакующих клеток в параметре fromSqs, если он не undefined.
*/
private calcAttacks(side: Colors.BW, target: Squares.Square, fromSqs?: Squares.Square[]): number {
const fromSquares = (fromSqs !== undefined) ? fromSqs : [];
let queen: Pieces.Piece;
let rook: Pieces.Piece;
let bishop: Pieces.Piece;
let knight: Pieces.Piece;
let delta: number;
let dest: Squares.Square;
let dirs: Directions.Direction[] = [];
let dir: Directions.Direction;
let last: Squares.Square | Squares.Empty;
let p: Pieces.Piece | Pieces.Empty;
let sq: Squares.Square | Squares.Empty;
let i: number;
// attacks Bishop/Queen/Rook: look at each of the 8 directions
if (side === Color.White) {
queen = Piece.WQueen;
rook = Piece.WRook;
bishop = Piece.WBishop;
knight = Piece.WKnight;
} else {
queen = Piece.BQueen;
rook = Piece.BRook;
bishop = Piece.BBishop;
knight = Piece.BKnight;
}
const numQueensRooks = this.material[queen] + this.material[rook];
const numQueensBishops = this.material[queen] + this.material[bishop];
// we only bother if there are any sliding pieces of each type:
if (numQueensRooks > 0) {
const fyle = Square.fyle(target);
const rank = Square.rank(target);
dirs = [];
if (this.fyleCount(queen, fyle) + this.fyleCount(rook, fyle) > 0) {
dirs.push(UP);
dirs.push(DOWN);
}
if (this.rankCount(queen, rank) + this.rankCount(rook, rank) > 0) {
dirs.push(LEFT);
dirs.push(RIGHT);
}
for (i = 0; i < dirs.length; i++) {
dir = dirs[i];
delta = Direction.delta(dir);
dest = target;
last = Square.last(target, dir);
while (dest !== last) {
dest = (dest + delta) as Squares.Square;
p = this.brd[dest];
if (p === noPiece) {
// square NOPIECE: keep searching
continue;
} else if (p === queen || p === rook) {
// found an attacking piece
fromSquares.push(dest);
break;
} else {
// found a piece, but not a queen or rook
break;
}
}
}
}
// now diagonal sliders: Queens/Bishops:
if (numQueensBishops > 0) {
const left = Square.leftDiag(target);
const right = Square.rightDiag(target);
dirs = [];
if (this.leftDiagCount(queen, left) + this.leftDiagCount(bishop, left) > 0) {
dirs.push(UP_LEFT);
dirs.push(DOWN_RIGHT);
}
if (this.rightDiagCount(queen, right) + this.rightDiagCount(bishop, right) > 0) {
dirs.push(UP_RIGHT);
dirs.push(DOWN_LEFT);
}
for (i = 0; i < dirs.length; i++) {
dir = dirs[i];
delta = Direction.delta(dir);
dest = target;
last = Square.last(target, dir);
while (dest !== last) {
dest = (dest + delta) as Squares.Square;
p = this.brd[dest];
if (p === noPiece) {
// square NOPIECE: keep searching
continue;
} else if (p === queen || p === bishop) {
// found an attacking piece
fromSquares.push(dest);
break;
} else {
// found a piece, but not an enemy queen or bishop
break;
}
}
}
}
// now knight checks: we only bother if there is a knight on the
// opposite square color of the target square color.
if ((this.material[knight] > 0) && (this.squareColorCount(knight, Color.flip(Square.color(target)))) > 0) {
const dests = Square.knightAttacks(target);
i = 0;
while (i < 20) {
const d = dests[i++];
if (d === ns) { break; }
if (this.brd[d] === knight) {
fromSquares.push(d);
}
}
}
// now pawn attacks:
if (side === Color.White) {
if (Square.rank(target) !== 0) { // if (Material[WP] > 0) {
sq = Square.move(target, DOWN_LEFT);
if (Square.isSquare(sq) && (this.brd[sq] === Piece.WPawn)) {
fromSquares.push(sq);
}
sq = Square.move(target, DOWN_RIGHT);
if (Square.isSquare(sq) && (this.brd[sq] === Piece.WPawn)) {
fromSquares.push(sq);
}
}
} else {
if (Square.rank(target) !== 7) { // if (Material[BP] > 0) {
sq = Square.move(target, UP_LEFT);
if (Square.isSquare(sq) && (this.brd[sq] === Piece.BPawn)) {
fromSquares.push(sq);
}
sq = Square.move(target, UP_RIGHT);
if (Square.isSquare(sq) && (this.brd[sq] === Piece.BPawn)) {
fromSquares.push(sq);
}
}
}
return fromSquares.length;
}
private calcPins(): void {
for (let i = 0; i < 16; i++) {
this.pinned[i] = NULL_DIR;
}
const kingSq = this.getKingSquare(this.wm);
const enemy = Color.flip(this.wm);
const enemyQueen = Piece.create(enemy, Piece.Queen);
const enemyRook = Piece.create(enemy, Piece.Rook);
const enemyBishop = Piece.create(enemy, Piece.Bishop);
// pins and checks from Bishops/Queens/Rooks:
const fyle = Square.fyle(kingSq);
if (this.fyleCount(enemyQueen, fyle) + this.fyleCount(enemyRook, fyle) > 0) {
this.calcPinsDir(UP, Piece.Rook);
this.calcPinsDir(DOWN, Piece.Rook);
}
const rank = Square.rank(kingSq);
if (this.rankCount(enemyQueen, rank) + this.rankCount(enemyRook, rank) > 0) {
this.calcPinsDir(LEFT, Piece.Rook);
this.calcPinsDir(RIGHT, Piece.Rook);
}
const ld = Square.leftDiag(kingSq);
if (this.leftDiagCount(enemyQueen, ld) + this.leftDiagCount(enemyBishop, ld) > 0) {
this.calcPinsDir(UP_LEFT, Piece.Bishop);
this.calcPinsDir(DOWN_RIGHT, Piece.Bishop);
}
const rd = Square.rightDiag(kingSq);
if (this.rightDiagCount(enemyQueen, rd) + this.rightDiagCount(enemyBishop, rd) > 0) {
this.calcPinsDir(UP_RIGHT, Piece.Bishop);
this.calcPinsDir(DOWN_LEFT, Piece.Bishop);
}
}
private calcPinsDir(dir: Directions.Direction, attacker: number): void {
// two pieces can pin along any path. A queen is always one,
// the other is a bishop or rook. To save calculating it here, the
// appropriate piece (BISHOP) or (ROOK) is passed along with the
// direction.
const king = this.getKingSquare(this.wm);
let friendly: Squares.Square | Squares.Empty = ns;
let x: Squares.Square = king;
const last = Square.last(king, dir);
const delta = Direction.delta(dir);
while (x !== last) {
x = (x + delta) as Squares.Square;
const p = this.brd[x];
if (p === noPiece) {
// square NOPIECE, so keep searching.
continue;
} else if (Piece.color(p) === this.wm) {
// found a friendly piece.
if (friendly === ns) {
// found first friendly in this direction
friendly = x;
} else {
// found second friendly in this direction, so stop.
return;
}
} else {
// found an enemy piece
if (friendly !== ns) {
// potential pin:
const ptype = Piece.type(p);
if (ptype === Piece.Queen || ptype === attacker) {
this.pinned[this.listPos[friendly]] = dir;
}
}
return; // found an enemy piece, so end search
}
}
}
/**
* Add legal move
*
* @param mlist
* @param from
* @param to
* @param promo
* @returns
*/
private addLegalMove(mlist: SimpleMove[], from: Squares.Square, to: Squares.Square, promo?: Pieces.PieceType) {
const sm = new SimpleMove();
// we do NOT set the pre-move castling/ep flags, or the captured
// piece info, here since that is ONLY needed if the move is
// going to be executed with DoSimpleMove() and then undone.
sm.from = from;
sm.to = to;
sm.promote = promo;
sm.movingPiece = this.brd[from];
sm.capturedPiece = this.brd[to];
mlist.push(sm);
}
/**
* Generate slider moves along a direction, for a sliding
* piece of the specified color from the specified square.
* If sqset != undefined, moves must be to a square in sqset.
*
* @param mlist
* @param color
* @param fromSq
* @param dir
* @param sqset
* @param capturesOnly
* @returns
*/
private genSliderMoves(mlist: SimpleMove[], color: Colors.BW, fromSq: Squares.Square, dir: Directions.Direction, capturesOnly?: boolean, sqset?: Squares.Square[]) {
capturesOnly = !!capturesOnly;
let dest: Squares.Square = fromSq;
const last = Square.last(fromSq, dir);
const delta = Direction.delta(dir);
while (dest !== last) {
dest = (dest + delta) as Squares.Square;
const p = this.brd[dest];
if (p === noPiece) {
if (!capturesOnly) {
if (is_valid_dest(dest, sqset)) {
this.addLegalMove(mlist, fromSq, dest, noPiece);
}
}
continue;
}
// we have reached a piece. Add the capture if it is an enemy.
if (Piece.color(p) !== color) {
if (is_valid_dest(dest, sqset)) {
this.addLegalMove(mlist, fromSq, dest, noPiece);
}
}
break;
}
}
/**
* Generate knight moves.
* If sqset != undefined, moves must be to a square in sqset.
*
* @param mlist
* @param color
* @param fromSq
* @param sqset
* @param capturesOnly
* @returns
*/
private genKnightMoves(mlist: SimpleMove[], color: Colors.BW, fromSq: Squares.Square, sqset?: Squares.Square[], capturesOnly?: boolean): void {
capturesOnly = !!capturesOnly;
const destArr = Square.knightAttacks(fromSq);
let i = 0;
while (true) {
const dest = destArr[i++];
if (dest === ns) { break; }
const p = this.brd[dest];
if (capturesOnly && (p === noPiece)) { continue; }
if ((p === noPiece) || (Piece.color(p) !== color)) {
if (is_valid_dest(dest, sqset)) {
this.addLegalMove(mlist, fromSq, dest, noPiece);
}
}
}
}
/**
* Generate the legal castling moves.
* Assumes the side to move is NOT in check, so the caller
* should verify this first.
*
* @param mlist
* @returns
*/
private genCastling(mlist: SimpleMove[]): void {
const from = this.getKingSquare(this.wm);
if (from !== (this.wm === Color.White ? 4 : 60)) { return; }
const enemyKingSq = this.getEnemyKingSquare();
let target: Squares.Square;
let skip: Squares.Square;
let rookSq: Squares.Square;
let rookPiece: Pieces.Piece;
// queen side Castling:
if (!this.strictCastling || this.castling.has(this.wm, CastlingSide.Queen)) {
if (this.wm === Color.White) {
target = 2; skip = 3; rookSq = 0; rookPiece = Piece.WRook;
} else {
target = 58; skip = 59; rookSq = 56; rookPiece = Piece.BRook;
}
if ((this.brd[target] === noPiece) && (this.brd[skip] === noPiece)
&& (this.brd[rookSq] === rookPiece)
&& (this.brd[target - 1] === noPiece) // squares B1 or B8 must be NOPIECE too!
&& (this.calcNumChecks(target) === 0)
&& (this.calcNumChecks(skip) === 0)
&& (!Square.adjacent(target, enemyKingSq))) {
this.addLegalMove(mlist, from, target, noPiece);
}
}
// king side Castling:
if (!this.strictCastling || this.castling.has(this.wm, CastlingSide.King)) {
if (this.wm === Color.White) {
target = 6;
skip = 5;
rookSq = 7;
rookPiece = Piece.WRook;
} else {
target = 62; skip = 61; rookSq = 63; rookPiece = Piece.BRook;
}
if (this.brd[target] === noPiece && this.brd[skip] === noPiece
&& (this.brd[rookSq] === rookPiece)
&& (this.calcNumChecks(target) === 0)
&& (this.calcNumChecks(skip) === 0)
&& (!Square.adjacent(target, enemyKingSq))) {
this.addLegalMove(mlist, from, target, noPiece);
}
}
}
/**
* Generate the legal King moves. Castling is generated as well, if
* the specified flag is true.
*
* @param mlist
* @param genType
* @param castling
* @returns
*/
private genKingMoves(mlist: SimpleMove[], genType: number, castling: boolean): void {
const kingSq = this.getKingSquare();
const enemyKingSq = this.getEnemyKingSquare();
const enemy = Color.flip(this.wm);
const king = Piece.create(this.wm, Piece.King);
const genNonCaptures = ((genType & GenerateMode.NonCaptures) !== 0);
const destArr = Square.kingAttacks(kingSq);
let i = 0;
let destSq: Squares.Square | Squares.Empty;
while ((destSq = destArr[i++]) !== ns) {
// try this move and see if it legal:
let addThisMove = false;
// only try this move if the target square has an enemy piece,
// or if it is NOPIECE and non captures are to be generated:
const captured = this.brd[destSq];
if ((genNonCaptures && !Piece.isPiece(captured)) ||
(Piece.isPiece(captured) && (Piece.color(captured) === enemy))) {
// enemy piece or NOPIECE there, so try the move:
this.brd[destSq] = king;
this.brd[kingSq] = noPiece;
// it is legal if the two kings will not be adjacent and the
// moving king will not be in check on its new square:
if (!Square.adjacent(destSq, enemyKingSq)) {
if (this.calcNumChecks(destSq) === 0) {
addThisMove = true;
}
}
this.brd[kingSq] = king;
this.brd[destSq] = captured;
}
if (addThisMove) {
this.addLegalMove(mlist, kingSq, destSq, noPiece);
}
}
// now generate castling moves, if possible:
if (genNonCaptures && castling) {
this.genCastling(mlist);
}
}
/**
* Add promotion moves.
* Called by GenPawnMoves() when a pawn can be promoted.
*
* @param mlist
* @param from
* @param dest
* @returns
*/
private addPromotions(mlist: SimpleMove[], from: Squares.Square, dest: Squares.Square): void {
this.addLegalMove(mlist, from, dest, Piece.Queen);
this.addLegalMove(mlist, from, dest, Piece.Rook);
this.addLegalMove(mlist, from, dest, Piece.Bishop);
this.addLegalMove(mlist, from, dest, Piece.Knight);
}
/**
* Used to verify that an enpassant pawn capture is valid.
* This is needed because illegal enpassant captures can appear legal
* according to calculations of pinned pieces.
* For example, consider WK d5, WP e5, BP f5 (just moved there),
* BR h5 and the enpassant capture exf6 would be illegal.
*
* @param from
* @param to
* @returns
*/
private isValidEnPassant(from: Squares.Square, to: Squares.Square): boolean {
// check that this enpassant capture is legal:
const ownPawn = Piece.create(this.wm, Piece.Pawn);
const enemyPawn = Piece.create(Color.flip(this.wm), Piece.Pawn);
const enemyPawnSq = (this.wm === Color.White) ? to - 8 : to + 8;
const toSqPiece = this.brd[to];
if ((this.brd[from] !== ownPawn) || (this.brd[enemyPawnSq] !== enemyPawn)) {
return false;
}
this.brd[from] = noPiece;
this.brd[to] = ownPawn;
this.brd[enemyPawnSq] = noPiece;
const isValid = !this.isKingInCheck();
this.brd[from] = ownPawn;
this.brd[to] = toSqPiece;
this.brd[enemyPawnSq] = enemyPawn;
return isValid;
}
private isPossibleCapture(dest: Squares.Square, from: Squares.Square) {
const p = this.brd[dest];
return (
(
Piece.isPiece(p) && (Piece.color(p) === (Color.flip(this.wm)))
) || (
(dest === this.EpTarget) && this.isValidEnPassant(from, dest)
)
);
}
/**
* Generate legal pawn moves.
* If dir != NULL_DIR, pawn MUST move in direction dir or its opposite.
* If sqset != undefined, pawn MUST move to a square in sqset.
* The dir and sq parameters are for pinned pawns and check evasions.
*
* @param mlist
* @param from
* @param dir
* @param sqset
* @param genType
* @returns
*/
private genPawnMoves(mlist: SimpleMove[], from: Squares.Square, dir: Directions.Direction, genType: number = GenerateMode.All, sqset?: Squares.Square[]) {
const genNonCaptures = ((genType & GenerateMode.NonCaptures) !== 0);
const oppdir = Direction.opposite(dir);
let forward: Directions.Direction;
let promoRank: Squares.Rank;
let secondRank: Squares.Rank;
let dest: Squares.Square | Squares.Empty;
if (this.wm === Color.White) {
forward = Direction.Up;
promoRank = 7;
secondRank = 1;
} else {
forward = DOWN;
promoRank = 0;
secondRank = 6;
}
if (genNonCaptures && (dir =