Fix bugs in move handling

This commit is contained in:
Mattia Giambirtone 2024-04-19 17:05:18 +02:00
parent fcbe15f275
commit 0496047164
2 changed files with 50 additions and 160 deletions

View File

@ -35,7 +35,7 @@ type
## A chess position
# Castling metadata. Updated on every move
castling: array[64, uint8]
castlingRights: array[64, uint8]
# Number of half-moves that were performed
# to reach this position starting from the
# root of the tree
@ -81,8 +81,8 @@ proc pretty*(self: ChessBoard): string
proc spawnPiece(self: ChessBoard, square: Square, piece: Piece)
proc toFEN*(self: ChessBoard): string
proc unmakeMove*(self: ChessBoard)
proc movePiece(self: ChessBoard, move: Move, attack: bool = true)
proc removePiece(self: ChessBoard, square: Square, attack: bool = true)
proc movePiece(self: ChessBoard, move: Move)
proc removePiece(self: ChessBoard, square: Square)
proc extend[T](self: var seq[T], other: openarray[T]) {.inline.} =
@ -163,6 +163,7 @@ proc newChessboard: ChessBoard =
new(result)
for i in 0..63:
result.grid[i] = nullPiece()
result.currPos = -1
result.position = Position(enPassantSquare: nullSquare(), turn: White)
# Indexing operations
@ -326,18 +327,18 @@ proc newChessboardFromFEN*(fen: string): ChessBoard =
discard
of 'K':
discard
# result.position.castlingAvailable.white.king = true
# result.position.castlingRightsAvailable.white.king = true
of 'Q':
discard
# result.position.castlingAvailable.white.queen = true
# result.position.castlingRightsAvailable.white.queen = true
of 'k':
discard
# result.position.castlingAvailable.black.king = true
# result.position.castlingRightsAvailable.black.king = true
of 'q':
discard
# result.position.castlingAvailable.black.queen = true
# result.position.castlingRightsAvailable.black.queen = true
else:
raise newException(ValueError, &"invalid FEN: unknown symbol '{c}' found in castling availability section")
raise newException(ValueError, &"invalid FEN: unknown symbol '{c}' found in castlingRights availability section")
of 3:
# En passant target square
case c:
@ -472,7 +473,7 @@ func isCastling*(move: Move): bool {.inline.} =
func getCastlingType*(move: Move): MoveFlag {.inline.} =
## Returns the castling type of the given move.
## Returns the castlingRights type of the given move.
## The return value of this function is only valid
## if isCastling() returns true
for flag in [CastleLong, CastleShort]:
@ -644,7 +645,7 @@ proc generatePawnMovements(self: ChessBoard, moves: var MoveList) =
for square in pawns.forwardRelativeTo(sideToMove) and allowedSquares:
moves.add(createMove(square.toBitboard().backwardRelativeTo(sideToMove), square))
# Double push
let rank = sideToMove.getFirstRank() # Only pawns on their starting rank can double push
let rank = if sideToMove == White: getRankMask(6) else: getRankMask(1) # Only pawns on their starting rank can double push
for square in (pawns and rank).doubleForwardRelativeTo(sideToMove) and allowedSquares:
moves.add(createMove(square.toBitboard().doubleBackwardRelativeTo(sideToMove), square, DoublePush))
@ -964,8 +965,8 @@ proc removePieceFromBitboard(self: ChessBoard, square: Square) =
self.position.pieces.white.queens.uint64.clearBit(square.int8)
of King:
self.position.pieces.white.king.uint64.clearBit(square.int8)
else:
discard
of Empty:
doAssert false, &"cannot remove empty white piece from {square}"
of Black:
case piece.kind:
of Pawn:
@ -979,11 +980,11 @@ proc removePieceFromBitboard(self: ChessBoard, square: Square) =
of Queen:
self.position.pieces.black.queens.uint64.clearBit(square.int8)
of King:
self.position.pieces.black.king.uint64.clearBit(square.int8)
else:
discard
self.position.pieces.black.king.uint64.clearBit(square.int8)
of Empty:
doAssert false, &"cannot remove empty black piece from {square}"
else:
discard
doAssert false, &"cannot remove empty piece from colorless square {square}"
proc addPieceToBitboard(self: ChessBoard, square: Square, piece: Piece) =
@ -1026,73 +1027,16 @@ proc addPieceToBitboard(self: ChessBoard, square: Square, piece: Piece) =
discard
proc removePiece(self: ChessBoard, square: Square, attack: bool = true) =
proc removePiece(self: ChessBoard, square: Square) =
## Removes a piece from the board, updating necessary
## metadata
var piece = self.grid[square]
self.grid[square] = nullPiece()
doAssert piece.kind != Empty and piece.color != None
self.removePieceFromBitboard(square)
#[if attack:
self.updateAttackedSquares()]#
self.grid[square] = nullPiece()
proc updateMovepieces(self: ChessBoard, move: Move) =
## Updates our bitboard representation after a move: note that this
## does *not* handle captures, en passant, promotions etc. as those
## are already called by helpers such as removePiece() and spawnPiece()
var bitboard: uint64
let piece = self.grid[move.startSquare]
# TODO: Should we use our helpers or is it faster to branch only once?
case piece.color:
of White:
case piece.kind:
of Pawn:
self.position.pieces.white.pawns.uint64.setBit(move.targetSquare.int8)
self.position.pieces.white.pawns.uint64.clearBit(move.startSquare.int8)
of Bishop:
self.position.pieces.white.bishops.uint64.setBit(move.targetSquare.int8)
self.position.pieces.white.bishops.uint64.clearBit(move.startSquare.int8)
of Knight:
self.position.pieces.white.knights.uint64.setBit(move.targetSquare.int8)
self.position.pieces.white.knights.uint64.clearBit(move.startSquare.int8)
of Rook:
self.position.pieces.white.rooks.uint64.setBit(move.targetSquare.int8)
self.position.pieces.white.rooks.uint64.clearBit(move.startSquare.int8)
of Queen:
self.position.pieces.white.queens.uint64.setBit(move.targetSquare.int8)
self.position.pieces.white.queens.uint64.clearBit(move.startSquare.int8)
of King:
self.position.pieces.white.king.uint64.setBit(move.targetSquare.int8)
self.position.pieces.white.king.uint64.clearBit(move.startSquare.int8)
else:
discard
of Black:
case piece.kind:
of Pawn:
self.position.pieces.black.pawns.uint64.setBit(move.targetSquare.int8)
self.position.pieces.black.pawns.uint64.clearBit(move.startSquare.int8)
of Bishop:
self.position.pieces.black.bishops.uint64.setBit(move.targetSquare.int8)
self.position.pieces.black.bishops.uint64.clearBit(move.startSquare.int8)
of Knight:
self.position.pieces.black.knights.uint64.setBit(move.targetSquare.int8)
self.position.pieces.black.knights.uint64.clearBit(move.startSquare.int8)
of Rook:
self.position.pieces.black.rooks.uint64.setBit(move.targetSquare.int8)
self.position.pieces.black.rooks.uint64.clearBit(move.startSquare.int8)
of Queen:
self.position.pieces.black.queens.uint64.setBit(move.targetSquare.int8)
self.position.pieces.black.queens.uint64.clearBit(move.startSquare.int8)
of King:
self.position.pieces.black.king.uint64.setBit(move.targetSquare.int8)
self.position.pieces.black.king.uint64.clearBit(move.startSquare.int8)
else:
discard
else:
discard
proc movePiece(self: ChessBoard, move: Move, attack: bool = true) =
proc movePiece(self: ChessBoard, move: Move) =
## Internal helper to move a piece. If attack
## is set to false, then this function does
## not update attacked squares metadata, just
@ -1100,20 +1044,15 @@ proc movePiece(self: ChessBoard, move: Move, attack: bool = true) =
let piece = self.grid[move.startSquare]
let targetSquare = self.getPiece(move.targetSquare)
if targetSquare.color != None:
raise newException(AccessViolationDefect, &"attempted to overwrite a piece! {move}")
raise newException(AccessViolationDefect, &"{piece} at {move.startSquare} attempted to overwrite {targetSquare} at {move.targetSquare}")
# Update positional metadata
self.updateMovePieces(move)
# Empty out the starting square
self.grid[move.startSquare] = nullPiece()
# Actually move the piece on the board
self.grid[move.targetSquare] = piece
#[if attack:
self.updateAttackedSquares()]#
self.removePiece(move.startSquare)
self.spawnPiece(move.targetSquare, piece)
proc movePiece(self: ChessBoard, startSquare, targetSquare: Square, attack: bool = true) =
proc movePiece(self: ChessBoard, startSquare, targetSquare: Square) =
## Like the other movePiece(), but with two squares
self.movePiece(Move(startSquare: startSquare, targetSquare: targetSquare), attack)
self.movePiece(Move(startSquare: startSquare, targetSquare: targetSquare))
proc doMove(self: ChessBoard, move: Move) =
@ -1124,14 +1063,16 @@ proc doMove(self: ChessBoard, move: Move) =
# Record final position for future reference
self.positions.add(self.position)
inc(self.currPos)
# Final checks
let piece = self.grid[move.startSquare]
doAssert piece.kind != Empty and piece.color != None
var
halfMoveClock = self.position.halfMoveClock
fullMoveCount = self.position.fullMoveCount
castling = self.position.castling
castlingRights = self.position.castlingRights
enPassantTarget = nullSquare()
# Needed to detect draw by the 50 move rule
if piece.kind == Pawn or move.isCapture() or move.isEnPassant():
@ -1144,69 +1085,26 @@ proc doMove(self: ChessBoard, move: Move) =
if move.isDoublePush():
enPassantTarget = move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare()
# Castling check: have the rooks moved?
if piece.kind == Rook:
discard
# case piece.color:
# of White:
# if rowFromSquare(move.startSquare) == piece.getStartRank():
# if columnFromSquare(move.startSquare) == 0:
# # Queen side
# castlingAvailable.white.queen = false
# elif columnfromSquare(move.startSquare) == 7:
# # King side
# castlingAvailable.white.king = false
# of Black:
# if rowFromSquare(move.startSquare) == piece.getStartRank():
# if columnFromSquare(move.startSquare) == 0:
# # Queen side
# castlingAvailable.black.queen = false
# elif columnFromSquare(move.startSquare) == 7:
# # King side
# castlingAvailable.black.king = false
# else:
# discard
# Has a rook been captured?
# Create new position
self.position = Position(plyFromRoot: self.position.plyFromRoot + 1,
halfMoveClock: halfMoveClock,
fullMoveCount: fullMoveCount,
turn: self.getSideToMove().opposite,
castling: castling,
castlingRights: castlingRights,
enPassantSquare: enPassantTarget,
pieces: self.position.pieces
)
# Update position metadata
if move.isCastling():
# Move the rook onto the
# correct file when castling
var
square: Square
target: Square
flag: MoveFlag
if move.getCastlingType() == CastleShort:
square = piece.color.kingSideRook()
target = shortCastleRook(piece.color)
flag = CastleShort
else:
square = piece.color.queenSideRook()
target = longCastleRook(piece.color)
flag = CastleLong
let rook = self.grid[square]
self.movePiece(createMove(square, target, flag), attack=false)
if move.isEnPassant():
# Make the en passant pawn disappear
self.removePiece(move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare(), attack=false)
self.removePiece(move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare())
if move.isCapture():
# Get rid of captured pieces
self.removePiece(move.targetSquare, attack=false)
# Move the piece to its target square and update attack metadata
self.movePiece(move, attack=false)
self.removePiece(move.targetSquare)
# Move the piece to its target square
self.movePiece(move)
if move.isPromotion():
# Move is a pawn promotion: get rid of the pawn
# and spawn a new piece
@ -1223,7 +1121,7 @@ proc doMove(self: ChessBoard, move: Move) =
else:
# Unreachable
discard
#self.updateAttackedSquares()
self.updateBoard()
proc spawnPiece(self: ChessBoard, square: Square, piece: Piece) =
@ -1261,26 +1159,18 @@ proc updateBoard*(self: ChessBoard) =
self.grid[sq] = Piece(color: White, kind: Queen)
for sq in self.position.pieces.black.queens:
self.grid[sq] = Piece(color: Black, kind: Queen)
self.grid[self.position.pieces.white.king.toSquare()] = Piece(color: White, kind: King)
self.grid[self.position.pieces.black.king.toSquare()] = Piece(color: Black, kind: King)
for sq in self.position.pieces.white.king:
self.grid[sq] = Piece(color: White, kind: King)
for sq in self.position.pieces.black.king:
self.grid[sq] = Piece(color: Black, kind: King)
proc unmakeMove*(self: ChessBoard) =
## Reverts to the previous board position,
## if one exists
if self.currPos > 0:
if self.currPos >= 0:
self.position = self.positions[self.currPos]
dec(self.currPos)
self.position = self.positions[self.currPos]
self.updateBoard()
proc redoMove*(self: ChessBoard) =
## Reverts to the next board position, if one
## exists. Only makes sense after a call to
## unmakeMove
if self.positions.high() > self.currPos:
inc(self.currPos)
self.position = self.positions[self.currPos]
self.updateBoard()
@ -1386,7 +1276,6 @@ proc pretty*(self: ChessBoard): string =
result &= "\x1b[0m"
proc toFEN*(self: ChessBoard): string =
## Returns a FEN string of the current
## position in the chessboard
@ -1413,8 +1302,8 @@ proc toFEN*(self: ChessBoard): string =
result &= " "
# Castling availability
result &= "-"
# let castleWhite = self.position.castlingAvailable.white
# let castleBlack = self.position.castlingAvailable.black
# let castleWhite = self.position.castlingRightsAvailable.white
# let castleBlack = self.position.castlingRightsAvailable.black
# if not (castleBlack.king or castleBlack.queen or castleWhite.king or castleWhite.queen):
# result &= "-"
# else:
@ -1623,13 +1512,13 @@ proc handleMoveCommand(board: ChessBoard, command: seq[string]): Move {.discarda
# Since the user tells us just the source and target square of the move,
# we have to figure out all the flags by ourselves (whether it's a double
# push, a capture, a promotion, castling, etc.)
# push, a capture, a promotion, etc.)
if board.grid[targetSquare].kind != Empty:
flags.add(Capture)
#elif board.grid[startSquare].kind == Pawn and abs(rowFromSquare(startSquare) - rowFromSquare(targetSquare)) == 2:
# flags.add(DoublePush)
elif board.grid[startSquare].kind == Pawn and abs(rankFromSquare(startSquare) - rankFromSquare(targetSquare)) == 2:
flags.add(DoublePush)
if len(moveString) == 5:
# Promotion
@ -1759,7 +1648,7 @@ const HELP_TEXT = """Nimfish help menu:
- position fen "..." moves a2a3 a7a6
- clear: Clear the screen
- move <move>: Perform the given move in algebraic notation
- castle: Print castling rights for each side
- castle: Print castlingRights rights for each side
- check: Print if the current side to move is in check
- undo, u: Undoes the last move. Can be used in succession
- turn: Print which side is to move
@ -1836,7 +1725,7 @@ proc main: int =
echo board.getPiece(cmd[1])
except ValueError:
echo "error: get: invalid square"
continue
continue
of "castle":
let canCastle = board.canCastle(board.getSideToMove())
echo &"Castling rights for {($board.getSideToMove()).toLowerAscii()}:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"

View File

@ -240,7 +240,7 @@ func computeKingBitboards: array[64, Bitboard] =
movements = movements or king.leftRelativeTo(White)
movements = movements or king.rightRelativeTo(White)
movements = movements or king.backwardRelativeTo(White)
movements = movements or king.forwardLeftRelativeTo(White)
movements = movements or king.forwardRightRelativeTo(White)
movements = movements or king.backwardRightRelativeTo(White)
movements = movements or king.backwardLeftRelativeTo(White)
# We don't *need* to mask the king off: the engine already masks off
@ -265,6 +265,7 @@ func computeKnightBitboards: array[64, Bitboard] =
movements = movements or knight.shortKnightDownRightRelativeTo(White)
movements = movements or knight.shortKnightUpLeftRelativeTo(White)
movements = movements or knight.shortKnightUpRightRelativeTo(White)
movements = movements and not knight
result[i] = movements