More work on attack handling and some bug fixes

This commit is contained in:
Mattia Giambirtone 2024-04-19 23:28:46 +02:00
parent 6fbcd4ff74
commit 2b16b5ec61
5 changed files with 165 additions and 267 deletions

View File

@ -14,8 +14,6 @@
import std/strutils import std/strutils
import std/strformat import std/strformat
import std/times
import std/math
import std/bitops import std/bitops
@ -29,8 +27,6 @@ export bitboards, magics, pieces, moves
type type
CountData = tuple[nodes: uint64, captures: uint64, castles: uint64, checks: uint64, promotions: uint64, enPassant: uint64, checkmates: uint64]
Position* = object Position* = object
## A chess position ## A chess position
@ -56,8 +52,8 @@ type
pieces: tuple[white, black: tuple[king, queens, rooks, bishops, knights, pawns: Bitboard]] pieces: tuple[white, black: tuple[king, queens, rooks, bishops, knights, pawns: Bitboard]]
# Pinned pieces for each side # Pinned pieces for each side
pins: tuple[white, black: Bitboard] pins: tuple[white, black: Bitboard]
# Checking pieces # Pieces checking the current side to move
checkers: tuple[white, black: Bitboard] checkers: Bitboard
ChessBoard* = ref object ChessBoard* = ref object
@ -83,10 +79,6 @@ proc movePiece(self: ChessBoard, move: Move)
proc removePiece(self: ChessBoard, square: Square) proc removePiece(self: ChessBoard, square: Square)
proc extend[T](self: var seq[T], other: openarray[T]) {.inline.} =
for x in other:
self.add(x)
proc update*(self: ChessBoard) proc update*(self: ChessBoard)
@ -121,27 +113,6 @@ func getHalfMoveCount*(self: ChessBoard): int {.inline.} =
return self.position.halfMoveClock return self.position.halfMoveClock
func getStartRank(piece: Piece): int {.inline.} =
## Retrieves the starting row of
## the given piece inside our 8x8
## grid
case piece.color:
of None:
return -1
of White:
case piece.kind:
of Pawn:
return 6
else:
return 7
of Black:
case piece.kind:
of Pawn:
return 1
else:
return 0
func getKingStartingSquare*(color: PieceColor): Square {.inline.} = func getKingStartingSquare*(color: PieceColor): Square {.inline.} =
## Retrieves the starting square of the king ## Retrieves the starting square of the king
## for the given color ## for the given color
@ -163,7 +134,7 @@ func longCastleRook*(color: PieceColor): Square {.inline.} = (if color == White:
func shortCastleRook*(color: PieceColor): Square {.inline.} = (if color == White: "f1".toSquare() else: "f8".toSquare()) func shortCastleRook*(color: PieceColor): Square {.inline.} = (if color == White: "f1".toSquare() else: "f8".toSquare())
proc inCheck*(self: ChessBoard, side: PieceColor): bool proc inCheck*(self: ChessBoard): bool
proc newChessboard: ChessBoard = proc newChessboard: ChessBoard =
@ -178,13 +149,7 @@ func `[]`(self: array[64, Piece], square: Square): Piece {.inline.} = self[squar
func `[]=`(self: var array[64, Piece], square: Square, piece: Piece) {.inline.} = self[square.int8] = piece func `[]=`(self: var array[64, Piece], square: Square, piece: Piece) {.inline.} = self[square.int8] = piece
func getDirectionMask(self: ChessBoard, square: Square, direction: Direction): Bitboard = func getBitboard*(self: ChessBoard, kind: PieceKind, color: PieceColor): Bitboard =
## Like getDirectionMask(), but used within the board context
## with a piece square and direction only
return getDirectionMask(square, self.grid[square].color, direction)
func getBitboard(self: ChessBoard, kind: PieceKind, color: PieceColor): Bitboard =
## Returns the positional bitboard for the given piece kind and color ## Returns the positional bitboard for the given piece kind and color
case color: case color:
of White: of White:
@ -223,7 +188,7 @@ func getBitboard(self: ChessBoard, kind: PieceKind, color: PieceColor): Bitboard
discard discard
func getBitboard(self: ChessBoard, piece: Piece): Bitboard = func getBitboard*(self: ChessBoard, piece: Piece): Bitboard =
## Returns the positional bitboard for the given piece type ## Returns the positional bitboard for the given piece type
return self.getBitboard(piece.kind, piece.color) return self.getBitboard(piece.kind, piece.color)
@ -242,7 +207,6 @@ proc newChessboardFromFEN*(fen: string): ChessBoard =
index = 0 index = 0
# Temporary variable to store a piece # Temporary variable to store a piece
piece: Piece piece: Piece
pieces: int
# See https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation # See https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation
while index <= fen.high(): while index <= fen.high():
var c = fen[index] var c = fen[index]
@ -376,9 +340,6 @@ proc newChessboardFromFEN*(fen: string): ChessBoard =
else: else:
raise newException(ValueError, "invalid FEN: too many fields in FEN string") raise newException(ValueError, "invalid FEN: too many fields in FEN string")
inc(index) inc(index)
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 == 0 or result.position.pieces.black.king == 0: if result.position.pieces.white.king == 0 or result.position.pieces.black.king == 0:
# Both kings must be on the board # Both kings must be on the board
raise newException(ValueError, "invalid position: exactly one king of each color must be present") raise newException(ValueError, "invalid position: exactly one king of each color must be present")
@ -511,18 +472,6 @@ func getFlags*(move: Move): seq[MoveFlag] =
result.add(Default) result.add(Default)
func getKingSquare*(self: ChessBoard, color: PieceColor): Square {.inline.} =
## Returns the square of the king for the given
## side
case color:
of White:
return self.position.pieces.white.king.toSquare()
of Black:
return self.position.pieces.black.king.toSquare()
else:
discard
proc getOccupancyFor(self: ChessBoard, color: PieceColor): Bitboard = proc getOccupancyFor(self: ChessBoard, color: PieceColor): Bitboard =
## Get the occupancy bitboard for every piece of the given color ## Get the occupancy bitboard for every piece of the given color
case color: case color:
@ -559,57 +508,48 @@ proc getKingAttacks(self: ChessBoard, square: Square, attacker: PieceColor): Bit
## the king of the given side ## the king of the given side
result = Bitboard(0) result = Bitboard(0)
let let
sq = square.toBitboard()
king = self.getBitboard(King, attacker) king = self.getBitboard(King, attacker)
if (KING_BITBOARDS[square.uint] and king) != 0: if (KING_BITBOARDS[square.uint] and king) != 0:
result = result or sq result = result or king
proc getKnightAttacks(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard = proc getKnightAttacks(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard =
## Returns the attack bitboard for the given square from ## Returns the attack bitboard for the given square from
## the knights of the given side ## the knights of the given side
let let
sq = square.toBitboard()
knights = self.getBitboard(Knight, attacker) knights = self.getBitboard(Knight, attacker)
result = Bitboard(0) result = Bitboard(0)
for knight in knights: for knight in knights:
if (KNIGHT_BITBOARDS[square.uint] and knight.toBitboard()) != 0: let knightBB = knight.toBitboard()
result = result or knight.toBitboard() if (KNIGHT_BITBOARDS[square.uint] and knightBB) != 0:
result = result or knightBB
proc getSlidingAttacks(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard = proc getSlidingAttacks(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard =
## Returns the attack bitboard for the given square from ## Returns the attack bitboard for the given square from
## the sliding pieces of the given side ## the sliding pieces of the given side
let let
sq = square.toBitboard()
queens = self.getBitboard(Queen, attacker) queens = self.getBitboard(Queen, attacker)
rooks = self.getBitboard(Rook, attacker) rooks = self.getBitboard(Rook, attacker) or queens
bishops = self.getBitboard(Bishop, attacker) bishops = self.getBitboard(Bishop, attacker) or queens
occupancy = self.getOccupancy()
result = Bitboard(0) result = Bitboard(0)
for rook in rooks: for rook in rooks:
let blockers = Rook.getRelevantBlockers(square) let blockers = Rook.getRelevantBlockers(square)
if (getRookMoves(square, blockers) and rook.toBitboard()) != 0: let rookBB = rook.toBitboard()
result = result or rook.toBitboard() if (getRookMoves(square, blockers) and rookBB) != 0:
result = result or rookBB
for bishop in bishops: for bishop in bishops:
let blockers = Bishop.getRelevantBlockers(square) let
if (getBishopMoves(square, blockers) and bishop.toBitboard()) != 0: blockers = Bishop.getRelevantBlockers(square)
result = result or bishop.toBitboard() bishopBB = bishop.toBitboard()
for queen in queens: if (getBishopMoves(square, blockers) and bishopBB) != 0:
let rookBlockers = Rook.getRelevantBlockers(square) result = result or bishopBB
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 = proc getAttacksTo*(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard =
## Computes the attack bitboard for the given square from ## Computes the attack bitboard for the given square from
## the given side ## the given side
result = Bitboard(0) result = Bitboard(0)
let
squareBitboard = square.toBitboard()
result = result or self.getPawnAttacks(square, attacker) result = result or self.getPawnAttacks(square, attacker)
result = result or self.getKingAttacks(square, attacker) result = result or self.getKingAttacks(square, attacker)
result = result or self.getKnightAttacks(square, attacker) result = result or self.getKnightAttacks(square, attacker)
@ -617,26 +557,15 @@ proc getAttacksTo*(self: ChessBoard, square: Square, attacker: PieceColor): Bitb
proc updateCheckers(self: ChessBoard) = proc updateCheckers(self: ChessBoard) =
let side = self.getSideToMove() let
let checkers = self.getAttacksTo(self.getKingSquare(side), side.opposite()) side = self.getSideToMove()
case side: king = self.getBitboard(King, side).toSquare()
of White: self.position.checkers = self.getAttacksTo(king, side.opposite())
self.position.checkers.white = checkers
of Black:
self.position.checkers.black = checkers
else:
discard
proc inCheck(self: ChessBoard, side: PieceColor): bool = proc inCheck(self: ChessBoard): bool =
## Returns if the current side to move is in check ## Returns if the current side to move is in check
case self.getSideToMove(): return self.position.checkers != 0
of White:
return self.position.checkers.white != 0
of Black:
return self.position.checkers.black != 0
else:
discard
proc canCastle*(self: ChessBoard, side: PieceColor): tuple[king, queen: bool] = proc canCastle*(self: ChessBoard, side: PieceColor): tuple[king, queen: bool] =
@ -644,14 +573,6 @@ proc canCastle*(self: ChessBoard, side: PieceColor): tuple[king, queen: bool] =
return (false, false) # TODO return (false, false) # TODO
proc getCapturablePieces(self: ChessBoard, side: PieceColor): Bitboard {.inline.} =
## Returns the set of pieces of the given color that can
## be captured
# Just a handy helper to filter out the king and avoid code duplication
return self.getOccupancyFor(side) and not self.getBitboard(King, side)
proc generatePawnMovements(self: ChessBoard, moves: var MoveList) = proc generatePawnMovements(self: ChessBoard, moves: var MoveList) =
## Helper of generatePawnMoves for generating all non-capture ## Helper of generatePawnMoves for generating all non-capture
## and non-promotion pawn moves ## and non-promotion pawn moves
@ -679,7 +600,7 @@ proc generatePawnCaptures(self: ChessBoard, moves: var MoveList) =
nonSideToMove = sideToMove.opposite() nonSideToMove = sideToMove.opposite()
pawns = self.getBitboard(Pawn, sideToMove) pawns = self.getBitboard(Pawn, sideToMove)
# We can only capture enemy pieces (except the king) # We can only capture enemy pieces (except the king)
enemyPieces = self.getCapturablePieces(nonSideToMove) enemyPieces = self.getOccupancyFor(nonSideToMove)
enemyPawns = self.getBitboard(Pawn, nonSideToMove) enemyPawns = self.getBitboard(Pawn, nonSideToMove)
rightMovement = pawns.forwardRightRelativeTo(sideToMove) rightMovement = pawns.forwardRightRelativeTo(sideToMove)
leftMovement = pawns.forwardLeftRelativeTo(sideToMove) leftMovement = pawns.forwardLeftRelativeTo(sideToMove)
@ -726,12 +647,12 @@ proc generateRookMoves(self: ChessBoard, moves: var MoveList) =
let let
sideToMove = self.getSideToMove() sideToMove = self.getSideToMove()
occupancy = self.getOccupancy() occupancy = self.getOccupancy()
enemyPieces = self.getCapturablePieces(sideToMove.opposite()) enemyPieces = self.getOccupancyFor(sideToMove.opposite())
rooks = self.getBitboard(Rook, sideToMove) rooks = self.getBitboard(Rook, sideToMove) or self.getBitboard(Queen, sideToMove)
for square in rooks: for square in rooks:
let blockers = occupancy and Rook.getRelevantBlockers(square) let
# Quiet moves blockers = occupancy and Rook.getRelevantBlockers(square)
var moveset = getRookMoves(square, blockers) moveset = getRookMoves(square, blockers)
for target in moveset and not occupancy: for target in moveset and not occupancy:
moves.add(createMove(square, target)) moves.add(createMove(square, target))
# Captures # Captures
@ -741,51 +662,27 @@ proc generateRookMoves(self: ChessBoard, moves: var MoveList) =
proc generateBishopMoves(self: ChessBoard, moves: var MoveList) = proc generateBishopMoves(self: ChessBoard, moves: var MoveList) =
## Helper of generateSlidingMoves to generate bishop moves ## Helper of generateSlidingMoves to generate bishop moves
# self.generateBishopMovements(moves)
# self.generateBishopCaptures(moves)
let let
sideToMove = self.getSideToMove() sideToMove = self.getSideToMove()
enemyPieces = self.getOccupancyFor(sideToMove.opposite()) enemyPieces = self.getOccupancyFor(sideToMove.opposite())
occupancy = self.getOccupancy() occupancy = self.getOccupancy()
bishops = self.getBitboard(Bishop, sideToMove) bishops = self.getBitboard(Bishop, sideToMove) or self.getBitboard(Queen, sideToMove)
for square in bishops: for square in bishops:
let blockers = occupancy and Bishop.getRelevantBlockers(square) let
let moveset = getBishopMoves(square, blockers) blockers = occupancy and Bishop.getRelevantBlockers(square)
# Can't move over other pieces moveset = getBishopMoves(square, blockers)
for target in moveset and not occupancy: for target in moveset and not occupancy:
moves.add(createMove(square, target)) moves.add(createMove(square, target))
for target in moveset and enemyPieces: for target in moveset and enemyPieces:
moves.add(createMove(square, target, Capture)) moves.add(createMove(square, target, Capture))
proc generateQueenMoves(self: ChessBoard, moves: var MoveList) =
## Helper of generateSlidingMoves to generate 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)
let moveset = rookMoves or bishopMoves
# Can't move over other pieces
for target in moveset and not occupancy:
moves.add(createMove(square, target))
for target in moveset and enemyPieces:
moves.add(createMove(square, target))
proc generateSlidingMoves(self: ChessBoard, moves: var MoveList) = proc generateSlidingMoves(self: ChessBoard, moves: var MoveList) =
## Generates all legal sliding moves for the side to move ## Generates all legal sliding moves for the side to move
self.generateRookMoves(moves) self.generateRookMoves(moves)
self.generateBishopMoves(moves) self.generateBishopMoves(moves)
self.generateQueenMoves(moves) # Queens are just handled rooks + bishops
proc generateKingMoves(self: ChessBoard, moves: var MoveList) = proc generateKingMoves(self: ChessBoard, moves: var MoveList) =
@ -794,11 +691,11 @@ proc generateKingMoves(self: ChessBoard, moves: var MoveList) =
sideToMove = self.getSideToMove() sideToMove = self.getSideToMove()
king = self.getBitboard(King, sideToMove) king = self.getBitboard(King, sideToMove)
moveIdx = king.toSquare().uint64 moveIdx = king.toSquare().uint64
allowedSquares = not self.getOccupancy() occupancy = self.getOccupancy()
nonSideToMove = sideToMove.opposite() nonSideToMove = sideToMove.opposite()
enemyPieces = self.getCapturablePieces(nonSideToMove) enemyPieces = self.getOccupancyFor(nonSideToMove)
# Regular moves # Regular moves
for square in KING_BITBOARDS[moveIdx] and allowedSquares: for square in KING_BITBOARDS[moveIdx] and not occupancy:
moves.add(createMove(king, square)) moves.add(createMove(king, square))
# Captures # Captures
for square in KING_BITBOARDS[moveIdx] and enemyPieces: for square in KING_BITBOARDS[moveIdx] and enemyPieces:
@ -810,12 +707,12 @@ proc generateKnightMoves(self: ChessBoard, moves: var MoveList)=
let let
sideToMove = self.getSideToMove() sideToMove = self.getSideToMove()
knights = self.getBitboard(Knight, sideToMove) knights = self.getBitboard(Knight, sideToMove)
allowedSquares = not self.getOccupancy() occupancy = self.getOccupancy()
nonSideToMove = sideToMove.opposite() nonSideToMove = sideToMove.opposite()
enemyPieces = self.getCapturablePieces(nonSideToMove) enemyPieces = self.getOccupancyFor(nonSideToMove)
for square in knights: for square in knights:
# Regular moves # Regular moves
for target in KNIGHT_BITBOARDS[square.uint64] and allowedSquares: for target in KNIGHT_BITBOARDS[square.uint64] and not occupancy:
moves.add(createMove(square, target)) moves.add(createMove(square, target))
# Captures # Captures
for target in KNIGHT_BITBOARDS[square.uint64] and enemyPieces: for target in KNIGHT_BITBOARDS[square.uint64] and enemyPieces:
@ -834,6 +731,7 @@ proc generateMoves*(self: ChessBoard, moves: var MoveList) =
self.generatePawnMoves(moves) self.generatePawnMoves(moves)
self.generateKnightMoves(moves) self.generateKnightMoves(moves)
self.generateSlidingMoves(moves) self.generateSlidingMoves(moves)
# self.updateCheckers()
proc removePieceFromBitboard(self: ChessBoard, square: Square) = proc removePieceFromBitboard(self: ChessBoard, square: Square) =
@ -942,11 +840,6 @@ proc movePiece(self: ChessBoard, move: Move) =
self.spawnPiece(move.targetSquare, piece) self.spawnPiece(move.targetSquare, piece)
proc movePiece(self: ChessBoard, startSquare, targetSquare: Square) =
## Like the other movePiece(), but with two squares
self.movePiece(Move(startSquare: startSquare, targetSquare: targetSquare))
proc doMove*(self: ChessBoard, move: Move) = proc doMove*(self: ChessBoard, move: Move) =
## Internal function called by makeMove after ## Internal function called by makeMove after
## performing legality checks. Can be used in ## performing legality checks. Can be used in
@ -1220,113 +1113,11 @@ proc toFEN*(self: ChessBoard): string =
when isMainModule: when isMainModule:
import nimfishpkg/tui
import nimfishpkg/misc
basicTests()
proc testPiece(piece: Piece, kind: PieceKind, color: PieceColor) =
doAssert piece.kind == kind and piece.color == color, &"expected piece of kind {kind} and color {color}, got {piece.kind} / {piece.color} instead"
proc testPieceCount(board: ChessBoard, kind: PieceKind, color: PieceColor, count: int) =
let pieces = board.countPieces(kind, color)
doAssert pieces == count, &"expected {count} pieces of kind {kind} and color {color}, got {pieces} instead"
proc testPieceBitboard(bitboard: Bitboard, squares: seq[Square]) =
var i = 0
for square in bitboard:
doAssert squares[i] == square, &"squares[{i}] != bitboard[i]: {squares[i]} != {square}"
inc(i)
if i != squares.len():
doAssert false, &"bitboard.len() ({i}) != squares.len() ({squares.len()})"
var b = newDefaultChessboard()
# Ensure correct number of pieces
testPieceCount(b, Pawn, White, 8)
testPieceCount(b, Pawn, Black, 8)
testPieceCount(b, Knight, White, 2)
testPieceCount(b, Knight, Black, 2)
testPieceCount(b, Bishop, White, 2)
testPieceCount(b, Bishop, Black, 2)
testPieceCount(b, Rook, White, 2)
testPieceCount(b, Rook, Black, 2)
testPieceCount(b, Queen, White, 1)
testPieceCount(b, Queen, Black, 1)
testPieceCount(b, King, White, 1)
testPieceCount(b, King, Black, 1)
# Ensure pieces are in the correct squares. This is testing the FEN
# parser
# Pawns
for loc in ["a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2"]:
testPiece(b.getPiece(loc), Pawn, White)
for loc in ["a7", "b7", "c7", "d7", "e7", "f7", "g7", "h7"]:
testPiece(b.getPiece(loc), Pawn, Black)
# Rooks
testPiece(b.getPiece("a1"), Rook, White)
testPiece(b.getPiece("h1"), Rook, White)
testPiece(b.getPiece("a8"), Rook, Black)
testPiece(b.getPiece("h8"), Rook, Black)
# Knights
testPiece(b.getPiece("b1"), Knight, White)
testPiece(b.getPiece("g1"), Knight, White)
testPiece(b.getPiece("b8"), Knight, Black)
testPiece(b.getPiece("g8"), Knight, Black)
# Bishops
testPiece(b.getPiece("c1"), Bishop, White)
testPiece(b.getPiece("f1"), Bishop, White)
testPiece(b.getPiece("c8"), Bishop, Black)
testPiece(b.getPiece("f8"), Bishop, Black)
# Kings
testPiece(b.getPiece("e1"), King, White)
testPiece(b.getPiece("e8"), King, Black)
# Queens
testPiece(b.getPiece("d1"), Queen, White)
testPiece(b.getPiece("d8"), Queen, Black)
# Ensure our bitboards match with the board
let
whitePawns = b.getBitboard(Pawn, White)
whiteKnights = b.getBitboard(Knight, White)
whiteBishops = b.getBitboard(Bishop, White)
whiteRooks = b.getBitboard(Rook, White)
whiteQueens = b.getBitboard(Queen, White)
whiteKing = b.getBitboard(King, White)
blackPawns = b.getBitboard(Pawn, Black)
blackKnights = b.getBitboard(Knight, Black)
blackBishops = b.getBitboard(Bishop, Black)
blackRooks = b.getBitboard(Rook, Black)
blackQueens = b.getBitboard(Queen, Black)
blackKing = b.getBitboard(King, Black)
whitePawnSquares = @[makeSquare(6'i8, 0'i8), makeSquare(6, 1), makeSquare(6, 2), makeSquare(6, 3), makeSquare(6, 4), makeSquare(6, 5), makeSquare(6, 6), makeSquare(6, 7)]
whiteKnightSquares = @[makeSquare(7'i8, 1'i8), makeSquare(7, 6)]
whiteBishopSquares = @[makeSquare(7'i8, 2'i8), makeSquare(7, 5)]
whiteRookSquares = @[makeSquare(7'i8, 0'i8), makeSquare(7, 7)]
whiteQueenSquares = @[makeSquare(7'i8, 3'i8)]
whiteKingSquares = @[makeSquare(7'i8, 4'i8)]
blackPawnSquares = @[makeSquare(1'i8, 0'i8), makeSquare(1, 1), makeSquare(1, 2), makeSquare(1, 3), makeSquare(1, 4), makeSquare(1, 5), makeSquare(1, 6), makeSquare(1, 7)]
blackKnightSquares = @[makeSquare(0'i8, 1'i8), makeSquare(0, 6)]
blackBishopSquares = @[makeSquare(0'i8, 2'i8), makeSquare(0, 5)]
blackRookSquares = @[makeSquare(0'i8, 0'i8), makeSquare(0, 7)]
blackQueenSquares = @[makeSquare(0'i8, 3'i8)]
blackKingSquares = @[makeSquare(0'i8, 4'i8)]
testPieceBitboard(whitePawns, whitePawnSquares)
testPieceBitboard(whiteKnights, whiteKnightSquares)
testPieceBitboard(whiteBishops, whiteBishopSquares)
testPieceBitboard(whiteRooks, whiteRookSquares)
testPieceBitboard(whiteQueens, whiteQueenSquares)
testPieceBitboard(whiteKing, whiteKingSquares)
testPieceBitboard(blackPawns, blackPawnSquares)
testPieceBitboard(blackKnights, blackKnightSquares)
testPieceBitboard(blackBishops, blackBishopSquares)
testPieceBitboard(blackRooks, blackRookSquares)
testPieceBitboard(blackQueens, blackQueenSquares)
testPieceBitboard(blackKing, blackKingSquares)
setControlCHook(proc () {.noconv.} = quit(0)) setControlCHook(proc () {.noconv.} = quit(0))
import tui quit(commandLoop())
quit(tui.commandLoop())

View File

@ -132,7 +132,7 @@ var
BISHOP_MOVES: array[64, seq[Bitboard]] BISHOP_MOVES: array[64, seq[Bitboard]]
proc getRookMoves*(square: Square, blockers: Bitboard): Bitboard = proc getRookMoves*(square: Square, blockers: Bitboard): Bitboard =
## Returns the move bitboard for the rook at the given ## Returns the move bitboard for the rook at the given
## square with the given blockers bitboard ## square with the given blockers bitboard
let let
@ -141,7 +141,7 @@ proc getRookMoves*(square: Square, blockers: Bitboard): Bitboard =
return moves[getIndex(magic, blockers)] return moves[getIndex(magic, blockers)]
proc getBishopMoves*(square: Square, blockers: Bitboard): Bitboard = proc getBishopMoves*(square: Square, blockers: Bitboard): Bitboard =
## Returns the move bitboard for the bishop at the given ## Returns the move bitboard for the bishop at the given
## square with the given blockers bitboard ## square with the given blockers bitboard
let let
@ -159,7 +159,7 @@ const
BISHOP_BLOCKERS = generateBishopBlockers() BISHOP_BLOCKERS = generateBishopBlockers()
func getRelevantBlockers*(kind: PieceKind, square: Square): Bitboard = func getRelevantBlockers*(kind: PieceKind, square: Square): Bitboard {.inline.} =
## Returns the relevant blockers mask for the given piece ## Returns the relevant blockers mask for the given piece
## type at the given square ## type at the given square
case kind: case kind:

View File

@ -0,0 +1,107 @@
import ../nimfish
import std/strformat
proc testPiece(piece: Piece, kind: PieceKind, color: PieceColor) =
doAssert piece.kind == kind and piece.color == color, &"expected piece of kind {kind} and color {color}, got {piece.kind} / {piece.color} instead"
proc testPieceCount(board: ChessBoard, kind: PieceKind, color: PieceColor, count: int) =
let pieces = board.countPieces(kind, color)
doAssert pieces == count, &"expected {count} pieces of kind {kind} and color {color}, got {pieces} instead"
proc testPieceBitboard(bitboard: Bitboard, squares: seq[Square]) =
var i = 0
for square in bitboard:
doAssert squares[i] == square, &"squares[{i}] != bitboard[i]: {squares[i]} != {square}"
inc(i)
if i != squares.len():
doAssert false, &"bitboard.len() ({i}) != squares.len() ({squares.len()})"
proc basicTests* =
var b = newDefaultChessboard()
# Ensure correct number of pieces
testPieceCount(b, Pawn, White, 8)
testPieceCount(b, Pawn, Black, 8)
testPieceCount(b, Knight, White, 2)
testPieceCount(b, Knight, Black, 2)
testPieceCount(b, Bishop, White, 2)
testPieceCount(b, Bishop, Black, 2)
testPieceCount(b, Rook, White, 2)
testPieceCount(b, Rook, Black, 2)
testPieceCount(b, Queen, White, 1)
testPieceCount(b, Queen, Black, 1)
testPieceCount(b, King, White, 1)
testPieceCount(b, King, Black, 1)
# Ensure pieces are in the correct squares. This is testing the FEN
# parser
# Pawns
for loc in ["a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2"]:
testPiece(b.getPiece(loc), Pawn, White)
for loc in ["a7", "b7", "c7", "d7", "e7", "f7", "g7", "h7"]:
testPiece(b.getPiece(loc), Pawn, Black)
# Rooks
testPiece(b.getPiece("a1"), Rook, White)
testPiece(b.getPiece("h1"), Rook, White)
testPiece(b.getPiece("a8"), Rook, Black)
testPiece(b.getPiece("h8"), Rook, Black)
# Knights
testPiece(b.getPiece("b1"), Knight, White)
testPiece(b.getPiece("g1"), Knight, White)
testPiece(b.getPiece("b8"), Knight, Black)
testPiece(b.getPiece("g8"), Knight, Black)
# Bishops
testPiece(b.getPiece("c1"), Bishop, White)
testPiece(b.getPiece("f1"), Bishop, White)
testPiece(b.getPiece("c8"), Bishop, Black)
testPiece(b.getPiece("f8"), Bishop, Black)
# Kings
testPiece(b.getPiece("e1"), King, White)
testPiece(b.getPiece("e8"), King, Black)
# Queens
testPiece(b.getPiece("d1"), Queen, White)
testPiece(b.getPiece("d8"), Queen, Black)
# Ensure our bitboards match with the board
let
whitePawns = b.getBitboard(Pawn, White)
whiteKnights = b.getBitboard(Knight, White)
whiteBishops = b.getBitboard(Bishop, White)
whiteRooks = b.getBitboard(Rook, White)
whiteQueens = b.getBitboard(Queen, White)
whiteKing = b.getBitboard(King, White)
blackPawns = b.getBitboard(Pawn, Black)
blackKnights = b.getBitboard(Knight, Black)
blackBishops = b.getBitboard(Bishop, Black)
blackRooks = b.getBitboard(Rook, Black)
blackQueens = b.getBitboard(Queen, Black)
blackKing = b.getBitboard(King, Black)
whitePawnSquares = @[makeSquare(6'i8, 0'i8), makeSquare(6, 1), makeSquare(6, 2), makeSquare(6, 3), makeSquare(6, 4), makeSquare(6, 5), makeSquare(6, 6), makeSquare(6, 7)]
whiteKnightSquares = @[makeSquare(7'i8, 1'i8), makeSquare(7, 6)]
whiteBishopSquares = @[makeSquare(7'i8, 2'i8), makeSquare(7, 5)]
whiteRookSquares = @[makeSquare(7'i8, 0'i8), makeSquare(7, 7)]
whiteQueenSquares = @[makeSquare(7'i8, 3'i8)]
whiteKingSquares = @[makeSquare(7'i8, 4'i8)]
blackPawnSquares = @[makeSquare(1'i8, 0'i8), makeSquare(1, 1), makeSquare(1, 2), makeSquare(1, 3), makeSquare(1, 4), makeSquare(1, 5), makeSquare(1, 6), makeSquare(1, 7)]
blackKnightSquares = @[makeSquare(0'i8, 1'i8), makeSquare(0, 6)]
blackBishopSquares = @[makeSquare(0'i8, 2'i8), makeSquare(0, 5)]
blackRookSquares = @[makeSquare(0'i8, 0'i8), makeSquare(0, 7)]
blackQueenSquares = @[makeSquare(0'i8, 3'i8)]
blackKingSquares = @[makeSquare(0'i8, 4'i8)]
testPieceBitboard(whitePawns, whitePawnSquares)
testPieceBitboard(whiteKnights, whiteKnightSquares)
testPieceBitboard(whiteBishops, whiteBishopSquares)
testPieceBitboard(whiteRooks, whiteRookSquares)
testPieceBitboard(whiteQueens, whiteQueenSquares)
testPieceBitboard(whiteKing, whiteKingSquares)
testPieceBitboard(blackPawns, blackPawnSquares)
testPieceBitboard(blackKnights, blackKnightSquares)
testPieceBitboard(blackBishops, blackBishopSquares)
testPieceBitboard(blackRooks, blackRookSquares)
testPieceBitboard(blackQueens, blackQueenSquares)
testPieceBitboard(blackKing, blackKingSquares)

View File

@ -9,8 +9,8 @@ type
PieceColor* = enum PieceColor* = enum
## A piece color enumeration ## A piece color enumeration
None = 0'i8, None = 0'i8
White, White
Black Black
PieceKind* = enum PieceKind* = enum

View File

@ -1,4 +1,4 @@
import nimfish import ../nimfish
import std/strformat import std/strformat
@ -20,7 +20,7 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
var moves = MoveList() var moves = MoveList()
self.generateMoves(moves) self.generateMoves(moves)
if not bulk: if not bulk:
if len(moves) == 0 and self.inCheck(self.getSideToMove()): if len(moves) == 0 and self.inCheck():
result.checkmates = 1 result.checkmates = 1
# TODO: Should we count stalemates/draws? # TODO: Should we count stalemates/draws?
if ply == 0: if ply == 0:
@ -54,7 +54,7 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
echo &"Turn: {self.getSideToMove()}" echo &"Turn: {self.getSideToMove()}"
echo &"Piece: {self.getPiece(move.startSquare).kind}" echo &"Piece: {self.getPiece(move.startSquare).kind}"
echo &"Flags: {move.getFlags()}" echo &"Flags: {move.getFlags()}"
echo &"In check: {(if self.inCheck(self.getSideToMove()): \"yes\" else: \"no\")}" echo &"In check: {(if self.inCheck(): \"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 &"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()}" echo &"Position before move: {self.toFEN()}"
stdout.write("En Passant target: ") stdout.write("En Passant target: ")
@ -73,13 +73,13 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
inc(result.promotions) inc(result.promotions)
if move.isEnPassant(): if move.isEnPassant():
inc(result.enPassant) inc(result.enPassant)
if self.inCheck(self.getSideToMove()): if self.inCheck():
# Opponent king is in check # Opponent king is in check
inc(result.checks) inc(result.checks)
if verbose: if verbose:
let canCastle = self.canCastle(self.getSideToMove()) let canCastle = self.canCastle(self.getSideToMove())
echo "\n" echo "\n"
echo &"Opponent in check: {(if self.inCheck(self.getSideToMove()): \"yes\" else: \"no\")}" 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\")}" 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 &"Position after move: {self.toFEN()}"
echo "\n", self.pretty() echo "\n", self.pretty()
@ -420,7 +420,7 @@ proc commandLoop*: int =
let canCastle = board.canCastle(board.getSideToMove()) 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\")}" 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": of "check":
echo &"{board.getSideToMove()} king in check: {(if board.inCheck(board.getSideToMove()): \"yes\" else: \"no\")}" echo &"{board.getSideToMove()} king in check: {(if board.inCheck(): \"yes\" else: \"no\")}"
else: else:
echo &"Unknown command '{cmd[0]}'. Type 'help' for more information." echo &"Unknown command '{cmd[0]}'. Type 'help' for more information."
except IOError: except IOError: