Fix bugs with pawn movegen and add promotions

This commit is contained in:
Mattia Giambirtone 2024-04-17 11:54:45 +02:00
parent e50cfb9d64
commit 86265c68f0
4 changed files with 75 additions and 76 deletions

View File

@ -46,21 +46,21 @@ func `!=`*(a: Bitboard, b: SomeInteger): bool {.inline.} = a.uint64 != b.uint64
func getFileMask*(file: int): Bitboard = Bitboard(0x101010101010101'u64) shl file.uint64
func getRankMask*(rank: int): Bitboard = Bitboard(uint64.high()) shl uint64(8 * (rank + 1))
func squareToBitboard*(square: SomeInteger): Bitboard = Bitboard(1'u64) shl square.uint64
func squareToBitboard*(square: Square): Bitboard = squareToBitboard(square.int8)
func getRankMask*(rank: int): Bitboard = Bitboard(0xff) shl uint64(8 * rank)
func toBitboard*(square: SomeInteger): Bitboard = Bitboard(1'u64) shl square.uint64
func toBitboard*(square: Square): Bitboard = toBitboard(square.int8)
proc bitboardToSquare*(b: Bitboard): Square = Square(b.uint64.countTrailingZeroBits())
proc toSquare*(b: Bitboard): Square = Square(b.uint64.countTrailingZeroBits())
func createMove*(startSquare: Bitboard, targetSquare: Square, flags: varargs[MoveFlag]): Move =
result = createMove(startSquare.bitboardToSquare(), targetSquare, flags)
result = createMove(startSquare.toSquare(), targetSquare, flags)
func createMove*(startSquare: Square, targetSquare: Bitboard, flags: varargs[MoveFlag]): Move =
result = createMove(startSquare, targetSquare.bitboardToSquare(), flags)
result = createMove(startSquare, targetSquare.toSquare(), flags)
func createMove*(startSquare, targetSquare: Bitboard, flags: varargs[MoveFlag]): Move =
result = createMove(startSquare.bitboardToSquare(), targetSquare.bitboardToSquare(), flags)
result = createMove(startSquare.toSquare(), targetSquare.toSquare(), flags)
func toBin*(x: Bitboard, b: Positive = 64): string = toBin(BiggestInt(x), b)
@ -73,7 +73,7 @@ iterator items*(self: Bitboard): Square =
## are set
var bits = self
while bits != 0:
yield bits.bitboardToSquare()
yield bits.toSquare()
bits = bits and bits - 1
@ -165,10 +165,10 @@ func getDirectionMask*(bitboard: Bitboard, color: PieceColor, direction: Directi
return bitboard shl 8
of Backward:
return bitboard shr 8
of ForwardLeft:
return bitboard shl 9
of ForwardRight:
return bitboard shl 7
of ForwardLeft:
return bitboard shr 9
of BackwardRight:
return bitboard shr 9
of BackwardLeft:
@ -179,34 +179,35 @@ func getDirectionMask*(bitboard: Bitboard, color: PieceColor, direction: Directi
discard
func getLastRank*(color: PieceColor): Bitboard = (if color == White: getRankMask(0) else: getRankMask(7))
func getDirectionMask*(square: Square, color: PieceColor, direction: Direction): Bitboard =
## Get a bitmask for the given direction for a piece
## of the given color located at the given square
result = getDirectionMask(squareToBitboard(square), color, direction)
result = getDirectionMask(toBitboard(square), color, direction)
func forwardRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = getDirectionMask(self, side, Forward)
func doubleForwardRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = self.forwardRelativeTo(side).forwardRelativeTo(side)
func backwardRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = getDirectionMask(self, side, Backward)
func doubleBackwardRelativeTo*(self: Bitboard, side: PieceColor): Bitboard = self.backwardRelativeTo(side).backwardRelativeTo(side)
# We mask off the first and last ranks/files for
# We mask off the first and last ranks for
# left and right movements respectively to
# avoid weird wraparounds
func topRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard =
func forwardRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard =
getDirectionMask(self, side, ForwardRight) and not getFileMask(7)
func topLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard =
func forwardLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard =
getDirectionMask(self, side, ForwardLeft) and not getFileMask(0)
func bottomRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard =
let lastRank = if side == White: getRankMask(0) else: getRankMask(7)
getDirectionMask(self, side, BackwardRight) and not lastRank
getDirectionMask(self, side, BackwardRight) and not getFileMask(7)
func bottomLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard =
let lastRank = if side == White: getRankMask(0) else: getRankMask(7)
getDirectionMask(self, side, BackwardLeft) and not getRankMask(7)
getDirectionMask(self, side, BackwardLeft) and not getFileMask(0)

View File

@ -85,9 +85,8 @@ proc updateBoard*(self: ChessBoard)
# A bunch of getters
func getActiveColor*(self: ChessBoard): PieceColor {.inline.} =
## Returns the currently active color
## (turn of who has to move)
func getSideToMove*(self: ChessBoard): PieceColor {.inline.} =
## Returns the currently side to move
return self.position.turn
@ -141,17 +140,6 @@ func getKingStartingSquare(color: PieceColor): Square {.inline.} =
discard
func getLastRank(color: PieceColor): int {.inline.} =
## Retrieves the square of the last
## rank relative to the given color
case color:
of White:
return 0
of Black:
return 7
else:
return -1
func kingSideRook(color: PieceColor): Square {.inline.} = (if color == White: makeSquare(7, 7) else: makeSquare(0, 7))
func queenSideRook(color: PieceColor): Square {.inline.} = (if color == White: makeSquare(7, 0) else: makeSquare(0, 0))
func longCastleKing(color: PieceColor): Square {.inline.} = (if color == White: makeSquare(7, 2) else: makeSquare(0, 5))
@ -369,7 +357,7 @@ proc newChessboardFromFEN*(fen: string): ChessBoard =
raise newException(ValueError, "invalid FEN: too many fields in FEN string")
inc(index)
# result.updateAttackedSquares()
#[if result.inCheck(result.getActiveColor().opposite):
#[if result.inCheck(result.getSideToMove().opposite):
# Opponent king cannot be captured on the next move
raise newException(ValueError, "invalid position: opponent king can be captured")]#
if result.position.pieces.white.king == Bitboard(0) or result.position.pieces.black.king == Bitboard(0):
@ -509,7 +497,7 @@ func getKing(self: ChessBoard, color: PieceColor = None): Square {.inline.} =
## color (if it is None, the active color is used)
var color = color
if color == None:
color = self.getActiveColor()
color = self.getSideToMove()
case color:
of White:
return Square(self.position.pieces.white.king.uint64.countTrailingZeroBits())
@ -541,54 +529,61 @@ proc getOccupancy(self: ChessBoard): Bitboard =
proc generatePawnMovements(self: ChessBoard, moves: var MoveList) =
## Helper of generatePawnMoves for generating all non-capture
## pawn movems
## and non-promotion pawn moves
let
sideToMove = self.getActiveColor()
sideToMove = self.getSideToMove()
pawns = self.getBitboard(Pawn, sideToMove)
# Can't move to these squares
occupancy = not self.getOccupancy()
offsets = if sideToMove == White: (-8, -16) else: (8, 16)
# Single pushes
# We can only move to squares that are *not* occupied by another piece.
# We also cannot move to the last rank, as that will result in a promotion
# and is handled elsewhere
occupancy = not (self.getOccupancy() or sideToMove.getLastRank())
# Single push
for square in pawns.forwardRelativeTo(sideToMove) and occupancy:
moves.add(createMove((square - offsets[0]), square))
# Double pushes
for square in pawns.doubleForwardRelativeTo(sideToMove) and occupancy:
moves.add(createMove((square - offsets[1]), square, DoublePush))
moves.add(createMove(square.toBitboard().backwardRelativeTo(sideToMove), square))
# 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 occupancy:
moves.add(createMove(square.toBitboard().doubleBackwardRelativeTo(sideToMove), square, DoublePush))
proc generatePawnCaptures(self: ChessBoard, moves: var MoveList) =
## Helper of generatePawnMoves for generating all capture
## pawn moves
let
sideToMove = self.getActiveColor()
sideToMove = self.getSideToMove()
nonSideToMove = sideToMove.opposite()
pawns = self.getBitboard(Pawn, sideToMove)
# We can only capture enemy pieces
enemyPieces = self.getOccupancyFor(nonSideToMove)
offsets = if sideToMove == White: (9, 7) else: (-7, -9)
rightMovement = pawns.topRightRelativeTo(sideToMove)
leftMovement = pawns.topLeftRelativeTo(sideToMove)
rightMovement = pawns.forwardRightRelativeTo(sideToMove)
leftMovement = pawns.forwardLeftRelativeTo(sideToMove)
epTarget = self.getEnPassantTarget()
let epBitboard = if (epTarget != nullSquare()): epTarget.squareToBitboard() else: Bitboard(0)
let epBitboard = if (epTarget != nullSquare()): epTarget.toBitboard() else: Bitboard(0)
# Top right attacks
for square in rightMovement and enemyPieces:
moves.add(createMove((square - offsets[0]), square, Capture))
# Top left attacks
moves.add(createMove(square.toBitboard().bottomLeftRelativeTo(sideToMove), square, Capture))
for square in leftMovement and enemyPieces:
moves.add(createMove((square - offsets[1]), square, Capture))
moves.add(createMove(square.toBitboard().bottomRightRelativeTo(sideToMove), square, Capture))
# Special case for en passant
let
epLeft = epBitboard and leftMovement
epRight = epBitboard and rightMovement
if epLeft != 0:
moves.add(createMove(epBitboard.topLeftRelativeTo(nonSideToMove), epBitboard, EnPassant))
moves.add(createMove(epBitboard.forwardLeftRelativeTo(nonSideToMove), epBitboard, EnPassant))
elif epRight != 0:
moves.add(createMove(epBitboard.topRightRelativeTo(nonSideToMove), epBitboard, EnPassant))
moves.add(createMove(epBitboard.forwardRightRelativeTo(nonSideToMove), epBitboard, EnPassant))
proc generatePawnPromotions(self: ChessBoard, moves: var MoveList) =
## Helper of generatePawnMoves for generating all pawn promotion
## moves
let
sideToMove = self.getSideToMove()
pawns = self.getBitboard(Pawn, sideToMove)
occupancy = not self.getOccupancy()
for square in pawns.forwardRelativeTo(sideToMove) and occupancy and sideToMove.getLastRank():
for promotion in [PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook]:
moves.add(createMove(square.toBitboard().backwardRelativeTo(sideToMove), square, promotion))
proc generatePawnMoves(self: ChessBoard, moves: var MoveList) =
@ -597,6 +592,7 @@ proc generatePawnMoves(self: ChessBoard, moves: var MoveList) =
self.generatePawnMovements(moves)
self.generatePawnCaptures(moves)
self.generatePawnPromotions(moves)
proc generateSlidingMoves(self: ChessBoard, square: Square): seq[Move] =
@ -882,7 +878,7 @@ proc doMove(self: ChessBoard, move: Move) =
inc(fullMoveCount)
if move.isDoublePush():
enPassantTarget = move.targetSquare.squareToBitboard().backwardRelativeTo(piece.color).bitboardToSquare()
enPassantTarget = move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare()
# Castling check: have the rooks moved?
if piece.kind == Rook:
@ -944,7 +940,7 @@ proc doMove(self: ChessBoard, move: Move) =
self.position = Position(plyFromRoot: self.position.plyFromRoot + 1,
halfMoveClock: halfMoveClock,
fullMoveCount: fullMoveCount,
turn: self.getActiveColor().opposite,
turn: self.getSideToMove().opposite,
castlingAvailable: castlingAvailable,
enPassantSquare: enPassantTarget,
pieces: self.position.pieces
@ -971,7 +967,7 @@ proc doMove(self: ChessBoard, move: Move) =
if move.isEnPassant():
# Make the en passant pawn disappear
self.removePiece(move.targetSquare.squareToBitboard().backwardRelativeTo(piece.color).bitboardToSquare(), attack=false)
self.removePiece(move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare(), attack=false)
if move.isCapture():
# Get rid of captured pieces
@ -1032,8 +1028,8 @@ 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.bitboardToSquare()] = Piece(color: White, kind: King)
self.grid[self.position.pieces.black.king.bitboardToSquare()] = Piece(color: Black, kind: King)
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)
proc unmakeMove*(self: ChessBoard) =
@ -1180,7 +1176,7 @@ proc toFEN*(self: ChessBoard): string =
result &= "/"
result &= " "
# Active color
result &= (if self.getActiveColor() == White: "w" else: "b")
result &= (if self.getSideToMove() == White: "w" else: "b")
result &= " "
# Castling availability
let castleWhite = self.position.castlingAvailable.white
@ -1245,10 +1241,10 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
for move in moves:
if verbose:
let canCastle = self.canCastle(self.getActiveColor())
let canCastle = self.canCastle(self.getSideToMove())
echo &"Ply (from root): {self.position.plyFromRoot}"
echo &"Move: {move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}, from ({move.startSquare.rank}, {move.startSquare.file}) to ({move.targetSquare.rank}, {move.targetSquare.file})"
echo &"Turn: {self.getActiveColor()}"
echo &"Turn: {self.getSideToMove()}"
echo &"Piece: {self.grid[move.startSquare].kind}"
echo &"Flags: {move.getFlags()}"
echo &"In check: {(if self.inCheck(): \"yes\" else: \"no\")}"
@ -1274,7 +1270,7 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
# Opponent king is in check
inc(result.checks)
if verbose:
let canCastle = self.canCastle(self.getActiveColor())
let canCastle = self.canCastle(self.getSideToMove())
echo "\n"
echo &"Opponent in check: {(if self.inCheck(): \"yes\" else: \"no\")}"
echo &"Opponent can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
@ -1420,7 +1416,7 @@ proc handleMoveCommand(board: ChessBoard, command: seq[string]): Move {.discarda
var move = createMove(startSquare, targetSquare, flags)
let piece = board.getPiece(move.startSquare)
if piece.kind == King and move.startSquare == board.getActiveColor().getKingStartingSquare():
if piece.kind == King and move.startSquare == board.getSideToMove().getKingStartingSquare():
if move.targetSquare == longCastleKing(piece.color):
move.flags = move.flags or CastleLong.uint16
elif move.targetSquare == shortCastleKing(piece.color):
@ -1582,7 +1578,7 @@ proc main: int =
of "undo", "u":
board.unmakeMove()
of "turn":
echo &"Active color: {board.getActiveColor()}"
echo &"Active color: {board.getSideToMove()}"
of "ep":
let target = board.getEnPassantTarget()
if target != nullSquare():
@ -1600,9 +1596,9 @@ proc main: int =
continue
#[of "castle":
let canCastle = board.canCastle()
echo &"Castling rights for {($board.getActiveColor()).toLowerAscii()}:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
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\")}"
of "check":
echo &"{board.getActiveColor()} king in check: {(if board.inCheck(): \"yes\" else: \"no\")}"
echo &"{board.getSideToMove()} king in check: {(if board.inCheck(): \"yes\" else: \"no\")}"
]#
else:
echo &"Unknown command '{cmd[0]}'. Type 'help' for more information."
@ -1719,18 +1715,18 @@ when isMainModule:
testPieceBitboard(blackQueens, blackQueenSquares)
testPieceBitboard(blackKing, blackKingSquares)
b = newChessboardFromFEN("B7/P1P1P1P1/1P1P1P1P/7k/8/8/8/3K4 w - - 0 1")
var m = MoveList()
b.generatePawnMoves(m)
echo &"Pawn moves for {b.getActiveColor()} at {b.toFEN()}: "
echo &"Pawn moves for {b.getSideToMove()} at {b.toFEN()}: "
for move in m:
echo " - ", move.startSquare, move.targetSquare
b.doMove(createMove("a2", "a3"))
echo " - ", move.startSquare, move.targetSquare, " ", move.getFlags()
#[b.doMove(createMove("d1", "c1"))
m.clear()
b.generatePawnMoves(m)
echo &"Pawn moves for {b.getActiveColor()} at {b.toFEN()}: "
echo &"Pawn moves for {b.getSideToMove()} at {b.toFEN()}: "
for move in m:
echo " - ", move.startSquare, move.targetSquare
b.doMove(createMove("a7", "a5"))
echo b.pretty()
echo " - ", move.startSquare, move.targetSquare, " ", move.getFlags()
echo b.pretty()]#
# setControlCHook(proc () {.noconv.} = quit(0))
# quit(main())

View File

@ -39,7 +39,9 @@ func `!=`*(a, b: Square): bool {.inline.} = a.int8 != b.int8
func `-`*(a, b: Square): Square {.inline.} = Square(a.int8 - b.int8)
func `-`*(a: Square, b: SomeInteger): Square {.inline.} = Square(a.int8 - b.int8)
func `-`*(a: SomeInteger, b: Square): Square {.inline.} = Square(a.int8 - b.int8)
func `+`*(a, b: Square): Square {.inline.} = Square(a.int8 + b.int8)
func `+`*(a: Square, b: SomeInteger): Square {.inline.} = Square(a.int8 + b.int8)
func `+`*(a: SomeInteger, b: Square): Square {.inline.} = Square(a.int8 + b.int8)
func colFromSquare*(square: Square): int8 = square.int8 mod 8 + 1
func rowFromSquare*(square: Square): int8 = square.int8 div 8 + 1

View File

@ -19,7 +19,7 @@ when isMainModule:
while true:
canCastle = board.canCastle()
echo &"{board.pretty()}"
echo &"Turn: {board.getActiveColor()}"
echo &"Turn: {board.getSideToMove()}"
echo &"Moves: {board.getMoveCount()} full, {board.getHalfMoveCount()} half"
echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
stdout.write(&"En passant target: ")