Add move generation for bishops and queens as well as attack tracking

This commit is contained in:
Mattia Giambirtone 2024-04-19 14:38:35 +02:00
parent 82cef11cc4
commit 19ad46bbda
2 changed files with 169 additions and 36 deletions

View File

@ -149,6 +149,8 @@ func shortCastleKing(color: PieceColor): Square {.inline.} = (if color == White:
func longCastleRook(color: PieceColor): Square {.inline.} = (if color == White: makeSquare(7, 3) else: makeSquare(7, 5))
func shortCastleRook(color: PieceColor): Square {.inline.} = (if color == White: makeSquare(0, 0) else: makeSquare(0, 2))
proc inCheck(self: ChessBoard, side: PieceColor): bool
proc newChessboard: ChessBoard =
## Returns a new, empty chessboard
@ -358,10 +360,9 @@ proc newChessboardFromFEN*(fen: string): ChessBoard =
else:
raise newException(ValueError, "invalid FEN: too many fields in FEN string")
inc(index)
# result.updateAttackedSquares()
#[if result.inCheck(result.getSideToMove().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")]#
raise newException(ValueError, "invalid position: opponent king can be captured")
if result.position.pieces.white.king == 0 or result.position.pieces.black.king == 0:
# Both kings must be on the board
raise newException(ValueError, "invalid position: exactly one king of each color must be present")
@ -548,7 +549,7 @@ proc getKingAttacks(self: ChessBoard, square: Square, attacker: PieceColor): Bit
let
sq = square.toBitboard()
king = self.getBitboard(King, attacker)
if (sq and KING_BITBOARDS[square.uint64]) != 0:
if (KING_BITBOARDS[square.uint] and king) != 0:
result = result or sq
@ -560,10 +561,37 @@ proc getKnightAttacks(self: ChessBoard, square: Square, attacker: PieceColor): B
knights = self.getBitboard(Knight, attacker)
result = Bitboard(0)
for knight in knights:
if (sq and KNIGHT_BITBOARDS[knight.uint64]) != 0:
if (KNIGHT_BITBOARDS[square.uint] and knight.toBitboard()) != 0:
result = result or knight.toBitboard()
proc getSlidingAttacks(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard =
## Returns the attack bitboard for the given square from
## the sliding pieces of the given side
let
sq = square.toBitboard()
queens = self.getBitboard(Queen, attacker)
rooks = self.getBitboard(Rook, attacker)
bishops = self.getBitboard(Bishop, attacker)
occupancy = self.getOccupancy()
result = Bitboard(0)
for rook in rooks:
let blockers = Rook.getRelevantBlockers(square)
if (getRookMoves(square, blockers) and rook.toBitboard()) != 0:
result = result or rook.toBitboard()
for bishop in bishops:
let blockers = Bishop.getRelevantBlockers(square)
if (getBishopMoves(square, blockers) and bishop.toBitboard()) != 0:
result = result or bishop.toBitboard()
for queen in queens:
let rookBlockers = Rook.getRelevantBlockers(square)
if (getRookMoves(square, rookBlockers) and queen.toBitboard()) != 0:
result = result or queen.toBitboard()
let bishopBlockers = Bishop.getRelevantBlockers(square)
if (getBishopMoves(square, bishopBlockers) and queen.toBitboard()) != 0:
result = result or queen.toBitboard()
proc getAttacksTo(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard =
## Computes the attack bitboard for the given square from
## the given side
@ -573,6 +601,17 @@ proc getAttacksTo(self: ChessBoard, square: Square, attacker: PieceColor): Bitbo
result = result or self.getPawnAttacks(square, attacker)
result = result or self.getKingAttacks(square, attacker)
result = result or self.getKnightAttacks(square, attacker)
result = result or self.getSlidingAttacks(square, attacker)
proc inCheck(self: ChessBoard, side: PieceColor): bool =
## Returns if the current side to move is in check
return self.getAttacksTo(self.getKingSquare(side), side.opposite()) != 0
proc canCastle(self: ChessBoard, side: PieceColor): tuple[king, queen: bool] =
## Returns if the current side to move can castle
return (false, false) # TODO
proc getCapturablePieces(self: ChessBoard, side: PieceColor): Bitboard {.inline.} =
@ -597,7 +636,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 = if sideToMove == White: getRankMask(6) else: getRankMask(1) # Only pawns on their starting rank can double push
let rank = sideToMove.getFirstRank() # 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))
@ -661,7 +700,7 @@ proc generateRookMovements(self: ChessBoard, moves: var MoveList) =
for square in rooks:
let blockers = occupancy and Rook.getRelevantBlockers(square)
var moveset = getRookMoves(square, blockers)
# Can't capture our own pieces
# Can't move over our own pieces
moveset = moveset and not friendlyPieces
for target in moveset:
moves.add(createMove(square, target))
@ -679,9 +718,10 @@ proc generateRookCaptures(self: ChessBoard, moves: var MoveList) =
let blockers = occupancy and Rook.getRelevantBlockers(square)
var moveset = getRookMoves(square, blockers)
# Can only cature enemy pieces
moveset = moveset and not enemyPieces
moveset = moveset and enemyPieces
for target in moveset:
moves.add(createMove(square, target))
moves.add(createMove(square, target, Capture))
proc generateRookMoves(self: ChessBoard, moves: var MoveList) =
## Helper of generateSlidingMoves to generate rook moves
@ -689,9 +729,103 @@ proc generateRookMoves(self: ChessBoard, moves: var MoveList) =
self.generateRookCaptures(moves)
proc generateBishopMovements(self: ChessBoard, moves: var MoveList) =
## Helper of generateBishopMoves to generate all non-capture
## bishop moves
let
sideToMove = self.getSideToMove()
occupancy = self.getOccupancy()
friendlyPieces = self.getOccupancyFor(sideToMove)
bishops = self.getBitboard(Bishop, sideToMove)
for square in bishops:
let blockers = occupancy and Bishop.getRelevantBlockers(square)
var moveset = getBishopMoves(square, blockers)
# Can't move over our own pieces
moveset = moveset and not friendlyPieces
for target in moveset:
moves.add(createMove(square, target))
proc generateBishopCaptures(self: ChessBoard, moves: var MoveList) =
## Helper of generateBishopMoves to generate all capture
## bishop moves
let
sideToMove = self.getSideToMove()
occupancy = self.getOccupancy()
enemyPieces = self.getCapturablePieces(sideToMove.opposite())
bishops = self.getBitboard(Bishop, sideToMove)
for square in bishops:
let blockers = occupancy and Bishop.getRelevantBlockers(square)
var moveset = getRookMoves(square, blockers)
# Can only cature enemy pieces
moveset = moveset and enemyPieces
for target in moveset:
moves.add(createMove(square, target, Capture))
proc generateBishopMoves(self: ChessBoard, moves: var MoveList) =
## Helper of generateSlidingMoves to generate bishop moves
self.generateBishopMovements(moves)
self.generateBishopCaptures(moves)
proc generateQueenMovements(self: ChessBoard, moves: var MoveList) =
## Helper of generateQueenMoves to generate all non-capture
## bishop moves
let
sideToMove = self.getSideToMove()
occupancy = self.getOccupancy()
friendlyPieces = self.getOccupancyFor(sideToMove)
queens = self.getBitboard(Queen, sideToMove)
for square in queens:
# A queen is just a rook plus a bishop in terms of move
# generation
let
rookBlockers = Rook.getRelevantBlockers(square) and occupancy
bishopBlockers = Bishop.getRelevantBlockers(square) and occupancy
rookMoves = getRookMoves(square, rookBlockers)
bishopMoves = getBishopMoves(square, bishopBlockers)
var moveset = rookMoves or bishopMoves
# Can't move over our own pieces
moveset = moveset and not friendlyPieces
for target in moveset:
moves.add(createMove(square, target))
proc generateQueenCaptures(self: ChessBoard, moves: var MoveList) =
## Helper of generateQueenMoves to generate all capture
## queen moves
let
sideToMove = self.getSideToMove()
occupancy = self.getOccupancy()
enemyPieces = self.getCapturablePieces(sideToMove.opposite())
queens = self.getBitboard(Queen, sideToMove)
for square in queens:
# A queen is just a rook plus a bishop in terms of move
# generation
let
rookBlockers = Rook.getRelevantBlockers(square) and occupancy
bishopBlockers = Bishop.getRelevantBlockers(square) and occupancy
rookMoves = getRookMoves(square, rookBlockers)
bishopMoves = getBishopMoves(square, bishopBlockers)
var moveset = rookMoves or bishopMoves
# Can only capture the enemy pieces
moveset = moveset and enemyPieces
for target in moveset:
moves.add(createMove(square, target, Capture))
proc generateQueenMoves(self: ChessBoard, moves: var MoveList) =
## Helper of generateSlidingMoves to generate queen moves
self.generateQueenMovements(moves)
self.generateQueenCaptures(moves)
proc generateSlidingMoves(self: ChessBoard, moves: var MoveList) =
## Generates all legal sliding moves for the side to move
self.generateRookMoves(moves)
self.generateBishopMoves(moves)
self.generateQueenMoves(moves)
proc generateKingMoves(self: ChessBoard, moves: var MoveList) =
@ -800,8 +934,7 @@ proc generateMoves*(self: ChessBoard, moves: var MoveList) =
self.generatePawnMoves(moves)
self.generateKingMoves(moves)
self.generateKnightMoves(moves)
self.generateRookMoves(moves)
# TODO: all pieces
self.generateSlidingMoves(moves)
proc removePieceFromBitboard(self: ChessBoard, square: Square) =
@ -1329,7 +1462,7 @@ proc toFEN*(self: ChessBoard): string =
# Fullmove number
result &= $self.getMoveCount()
#[
proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = false, bulk: bool = false): CountData =
## Counts (and debugs) the number of legal positions reached after
## the given number of ply
@ -1337,7 +1470,7 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
var moves = MoveList()
self.generateMoves(moves)
if not bulk:
if len(moves) == 0 and self.inCheck():
if len(moves) == 0 and self.inCheck(self.getSideToMove()):
result.checkmates = 1
# TODO: Should we count stalemates/draws?
if ply == 0:
@ -1367,11 +1500,11 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
if verbose:
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 &"Move: {move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}"
echo &"Turn: {self.getSideToMove()}"
echo &"Piece: {self.grid[move.startSquare].kind}"
echo &"Flags: {move.getFlags()}"
echo &"In check: {(if self.inCheck(): \"yes\" else: \"no\")}"
echo &"In check: {(if self.inCheck(self.getSideToMove()): \"yes\" else: \"no\")}"
echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
echo &"Position before move: {self.toFEN()}"
stdout.write("En Passant target: ")
@ -1390,13 +1523,13 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
inc(result.promotions)
if move.isEnPassant():
inc(result.enPassant)
if self.inCheck():
if self.inCheck(self.getSideToMove()):
# Opponent king is in check
inc(result.checks)
if verbose:
let canCastle = self.canCastle(self.getSideToMove())
echo "\n"
echo &"Opponent in check: {(if self.inCheck(): \"yes\" else: \"no\")}"
echo &"Opponent in check: {(if self.inCheck(self.getSideToMove()): \"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\")}"
echo &"Position after move: {self.toFEN()}"
echo "\n", self.pretty()
@ -1433,7 +1566,7 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
result.castles += next.castles
result.enPassant += next.enPassant
result.checkmates += next.checkmates
]#
proc handleGoCommand(board: ChessBoard, command: seq[string]) =
if len(command) < 2:
@ -1462,7 +1595,7 @@ proc handleGoCommand(board: ChessBoard, command: seq[string]) =
break
if not ok:
return
#[try:
try:
let ply = parseInt(args[0])
if bulk:
let t = cpuTime()
@ -1485,7 +1618,6 @@ proc handleGoCommand(board: ChessBoard, command: seq[string]) =
echo "Error: go: perft: invalid depth"
else:
echo &"Error: go: unknown subcommand '{command[1]}'"
]#
proc handleMoveCommand(board: ChessBoard, command: seq[string]): Move {.discardable.} =
@ -1660,6 +1792,7 @@ const HELP_TEXT = """Nimfish help menu:
- fen: Shorthand for "position fen"
- pos <args>: Shorthand for "position <args>"
- get <square>: Get the piece on the given square
- atk: Print the attack bitboard of the given square for the side to move
- uci: enter UCI mode (WIP)
"""
@ -1703,6 +1836,15 @@ proc main: int =
board.unmakeMove()
of "turn":
echo &"Active color: {board.getSideToMove()}"
of "atk":
if len(cmd) != 2:
echo "error: atk: invalid number of arguments"
continue
try:
echo board.getAttacksTo(cmd[1].toSquare(), board.getSideToMove())
except ValueError:
echo "error: atk: invalid square"
continue
of "ep":
let target = board.getEnPassantTarget()
if target != nullSquare():
@ -1718,12 +1860,11 @@ proc main: int =
except ValueError:
echo "error: get: invalid square"
continue
#[of "castle":
let canCastle = board.canCastle()
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\")}"
of "check":
echo &"{board.getSideToMove()} king in check: {(if board.inCheck(): \"yes\" else: \"no\")}"
]#
echo &"{board.getSideToMove()} king in check: {(if board.inCheck(board.getSideToMove()): \"yes\" else: \"no\")}"
else:
echo &"Unknown command '{cmd[0]}'. Type 'help' for more information."
except IOError:
@ -1839,14 +1980,5 @@ when isMainModule:
testPieceBitboard(blackQueens, blackQueenSquares)
testPieceBitboard(blackKing, blackKingSquares)
b = newChessboardFromFEN("8/5R2/8/8/7k/8/5K2/8 w - - 0 1")
var m = MoveList()
b.generateRookMovements(m)
echo &"There are {len(m)} legal moves for {b.getSideToMove()} at {b.toFEN()}: "
for move in m:
echo " - ", move.startSquare, move.targetSquare, " ", move.getFlags()
echo b.pretty()
# setControlCHook(proc () {.noconv.} = quit(0))
# quit(main())
setControlCHook(proc () {.noconv.} = quit(0))
quit(main())

View File

@ -342,9 +342,10 @@ else:
var path = joinPath(getCurrentDir(), "src/resources")
if path.lastPathPart() == "nimfish":
path = joinPath("src", path)
echo "Loading magic bitboards"
var magics = readFile(joinPath(path, "magics.json")).fromJson(TableRef[string, array[64, MagicEntry]])
var moves = readFile(joinPath(path, "movesets.json")).fromJson(TableRef[string, array[64, seq[Bitboard]]])
ROOK_MAGICS = magics["rooks"]
BISHOP_MAGICS = magics["bishops"]
ROOK_MOVES = moves["rooks"]
BISHOP_MOVES = moves["bishops"]
BISHOP_MOVES = moves["bishops"]