diff --git a/__tests__/nullmove.test.ts b/__tests__/nullmove.test.ts new file mode 100644 index 00000000..9bcc969c --- /dev/null +++ b/__tests__/nullmove.test.ts @@ -0,0 +1,9 @@ +import { Chess } from '../src/chess' + +test('null move', () => { + const fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1' + const next = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1' + const chess = new Chess(fen) + chess.move('--') + expect(chess.fen()).toBe(next) +}) diff --git a/src/chess.ts b/src/chess.ts index cc4719f6..0e551f73 100644 --- a/src/chess.ts +++ b/src/chess.ts @@ -101,6 +101,7 @@ const FLAGS: Record = { PROMOTION: 'p', KSIDE_CASTLE: 'k', QSIDE_CASTLE: 'q', + NULL_MOVE: 'nm', } // prettier-ignore @@ -123,6 +124,7 @@ const BITS: Record = { PROMOTION: 16, KSIDE_CASTLE: 32, QSIDE_CASTLE: 64, + NULL_MOVE: 128, } /* @@ -265,6 +267,8 @@ const SECOND_RANK = { b: RANK_7, w: RANK_2 } const TERMINATION_MARKERS = ['1-0', '0-1', '1/2-1/2', '*'] +const SAN_NULLMOVE = '--' + // Extracts the zero-based rank of an 0x88 square. function rank(square: number): number { return square >> 4 @@ -1405,6 +1409,18 @@ export class Chess { const them = swapColor(us) this._push(move) + if (move.flags & BITS.NULL_MOVE){ + if (us === BLACK) { + this._moveNumber++ + } + + this._turn = them + + this._epSquare = EMPTY + + return + } + this._board[move.to] = this._board[move.from] delete this._board[move.from] @@ -1524,6 +1540,10 @@ export class Chess { const us = this._turn const them = swapColor(us) + if (move.flags & (BITS.NULL_MOVE)) { + return move + } + this._board[move.from] = this._board[move.to] this._board[move.from].type = move.piece // to undo any promotions delete this._board[move.to] @@ -1961,6 +1981,8 @@ export class Chess { output = 'O-O' } else if (move.flags & BITS.QSIDE_CASTLE) { output = 'O-O-O' + } else if (move.flags & BITS.NULL_MOVE){ + output = SAN_NULLMOVE } else { if (move.piece !== PAWN) { const disambiguator = getDisambiguator(move, moves) @@ -1981,15 +2003,17 @@ export class Chess { } } - this._makeMove(move) - if (this.isCheck()) { - if (this.isCheckmate()) { - output += '#' - } else { - output += '+' + if (!(move.flags & BITS.NULL_MOVE)){ + this._makeMove(move) + if (this.isCheck()) { + if (this.isCheckmate()) { + output += '#' + } else { + output += '+' + } } + this._undoMove() } - this._undoMove() return output } @@ -1999,6 +2023,18 @@ export class Chess { // strip off any move decorations: e.g Nf3+?! becomes Nf3 const cleanMove = strippedSan(move) + //first implementation of null with a dummy move (black king moves from a8 to a8), maybe this can be implemented better + if (cleanMove == SAN_NULLMOVE){ + let res: InternalMove = { + color: this._turn, + from: 0, + to: 0, + piece: "k", + flags: BITS.NULL_MOVE, + }; + return res + } + let pieceType = inferPieceType(cleanMove) let moves = this._moves({ legal: true, piece: pieceType }) @@ -2202,9 +2238,13 @@ export class Chess { } // generate the FEN for the 'after' key - this._makeMove(uglyMove) - move.after = this.fen() - this._undoMove() + if (!(uglyMove.flags & BITS.NULL_MOVE)){ + this._makeMove(uglyMove) + move.after = this.fen() + this._undoMove() + } else { + move.after = this.fen() + } if (captured) { move.captured = captured