diff --git a/Chess/nimfish/nimfishpkg/board.nim b/Chess/nimfish/nimfishpkg/board.nim new file mode 100644 index 0000000..36d5c6e --- /dev/null +++ b/Chess/nimfish/nimfishpkg/board.nim @@ -0,0 +1,486 @@ +import std/strformat +import std/strutils + + +import pieces +import magics +import moves +import rays +import bitboards +import position + + +export pieces, position, bitboards, moves, magics, rays + + + + +type + Chessboard* = ref object + ## A chessboard + + # The actual board where pieces live + grid*: array[64, Piece] + # The current position + position*: Position + # List of all previously reached positions + positions*: seq[Position] + + +# A bunch of simple utility functions and forward declarations +proc toFEN*(self: Chessboard): string + + +proc newChessboard: Chessboard = + ## Returns a new, empty chessboard + new(result) + for i in 0..63: + result.grid[i] = nullPiece() + result.position = Position(enPassantSquare: nullSquare(), sideToMove: White) + + +# Indexing operations +func `[]`*(self: array[64, Piece], square: Square): Piece {.inline.} = self[square.int8] +func `[]=`*(self: var array[64, Piece], square: Square, piece: Piece) {.inline.} = self[square.int8] = piece + + +func getBitboard*(self: Chessboard, kind: PieceKind, color: PieceColor): Bitboard {.inline.} = + ## Returns the positional bitboard for the given piece kind and color + return self.position.getBitboard(kind, color) + + +func getBitboard*(self: Chessboard, piece: Piece): Bitboard {.inline.} = + ## Returns the positional bitboard for the given piece type + return self.getBitboard(piece.kind, piece.color) + + +proc newChessboardFromFEN*(fen: string): Chessboard = + ## Initializes a chessboard with the + ## position encoded by the given FEN string + result = newChessboard() + var + # Current square in the grid + row: int8 = 0 + column: int8 = 0 + # Current section in the FEN string + section = 0 + # Current index into the FEN string + index = 0 + # Temporary variable to store a piece + piece: Piece + # See https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation + while index <= fen.high(): + var c = fen[index] + if c == ' ': + # Next section + inc(section) + inc(index) + continue + case section: + of 0: + # Piece placement data + case c.toLowerAscii(): + # Piece + of 'r', 'n', 'b', 'q', 'k', 'p': + let square = makeSquare(row, column) + piece = c.fromChar() + result.position.pieces[piece.color][piece.kind][].setBit(square) + result.grid[square] = piece + inc(column) + of '/': + # Next row + inc(row) + column = 0 + of '0'..'9': + # Skip x columns + let x = int(uint8(c) - uint8('0')) + if x > 8: + raise newException(ValueError, &"invalid FEN: invalid column skip size ({x} > 8)") + column += int8(x) + else: + raise newException(ValueError, &"invalid FEN: unknown piece identifier '{c}'") + of 1: + # Active color + case c: + of 'w': + result.position.sideToMove = White + of 'b': + result.position.sideToMove = Black + else: + raise newException(ValueError, &"invalid FEN: invalid active color identifier '{c}'") + of 2: + # Castling availability + case c: + # TODO + of '-': + discard + of 'K': + result.position.castlingAvailability.white.king = true + of 'Q': + result.position.castlingAvailability.white.queen = true + of 'k': + result.position.castlingAvailability.black.king = true + of 'q': + result.position.castlingAvailability.black.queen = true + else: + raise newException(ValueError, &"invalid FEN: unknown symbol '{c}' found in castlingRights availability section") + of 3: + # En passant target square + case c: + of '-': + # Field is already uninitialized to the correct state + discard + else: + result.position.enPassantSquare = fen[index..index+1].toSquare() + # Square metadata is 2 bytes long + inc(index) + of 4: + # Halfmove clock + var s = "" + while not fen[index].isSpaceAscii(): + s.add(fen[index]) + inc(index) + # Backtrack so the space is seen by the + # next iteration of the loop + dec(index) + result.position.halfMoveClock = parseInt(s).int8 + of 5: + # Fullmove number + var s = "" + while index <= fen.high(): + s.add(fen[index]) + inc(index) + result.position.fullMoveCount = parseInt(s).int8 + else: + raise newException(ValueError, "invalid FEN: too many fields in FEN string") + inc(index) + + +proc newDefaultChessboard*: Chessboard {.inline.} = + ## Initializes a chessboard with the + ## starting position + return newChessboardFromFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") + + +func countPieces*(self: Chessboard, kind: PieceKind, color: PieceColor): int {.inline.} = + ## Returns the number of pieces with + ## the given color and type in the + ## current position + return self.position.pieces[color][kind][].countSquares() + + +func countPieces*(self: Chessboard, piece: Piece): int {.inline.} = + ## Returns the number of pieces on the board that + ## are of the same type and color as the given piece + return self.countPieces(piece.kind, piece.color) + + +func getPiece*(self: Chessboard, square: Square): Piece {.inline.} = + ## Gets the piece at the given square + return self.grid[square] + + +func getPiece*(self: Chessboard, square: string): Piece {.inline.} = + ## Gets the piece on the given square + ## in algebraic notation + return self.getPiece(square.toSquare()) + + +func getOccupancyFor*(self: Chessboard, color: PieceColor): Bitboard = + ## Get the occupancy bitboard for every piece of the given color + result = Bitboard(0) + for b in self.position.pieces[color][]: + result = result or b + + +func getOccupancy*(self: Chessboard): Bitboard {.inline.} = + ## Get the occupancy bitboard for every piece on + ## the chessboard + result = self.getOccupancyFor(Black) or self.getOccupancyFor(White) + + +func getPawnAttacks*(self: Chessboard, square: Square, attacker: PieceColor): Bitboard {.inline.} = + ## Returns the locations of the pawns attacking the given square + let + sq = square.toBitboard() + pawns = self.getBitboard(Pawn, attacker) + bottomLeft = sq.backwardLeftRelativeTo(attacker) + bottomRight = sq.backwardRightRelativeTo(attacker) + return pawns and (bottomLeft or bottomRight) + + +func getKingAttacks*(self: Chessboard, square: Square, attacker: PieceColor): Bitboard {.inline.} = + ## Returns the location of the king if it is attacking the given square + result = Bitboard(0) + let + king = self.getBitboard(King, attacker) + if (getKingAttacks(square) and king) != 0: + result = result or king + + +func getKnightAttacks*(self: Chessboard, square: Square, attacker: PieceColor): Bitboard = + ## Returns the locations of the knights attacking the given square + let + knights = self.getBitboard(Knight, attacker) + result = Bitboard(0) + for knight in knights: + let knightBB = knight.toBitboard() + if (getKnightAttacks(knight) and knightBB) != 0: + result = result or knightBB + + +proc getSlidingAttacks*(self: Chessboard, square: Square, attacker: PieceColor): Bitboard = + ## Returns the locations of the sliding pieces attacking the given square + let + queens = self.getBitboard(Queen, attacker) + rooks = self.getBitboard(Rook, attacker) or queens + bishops = self.getBitboard(Bishop, attacker) or queens + occupancy = self.getOccupancy() + squareBB = square.toBitboard() + result = Bitboard(0) + for rook in rooks: + let + blockers = occupancy and Rook.getRelevantBlockers(rook) + moves = getRookMoves(rook, blockers) + # Attack set intersects our chosen square + if (moves and squareBB) != 0: + result = result or rook.toBitboard() + for bishop in bishops: + let + blockers = occupancy and Bishop.getRelevantBlockers(bishop) + moves = getBishopMoves(bishop, blockers) + if (moves and squareBB) != 0: + result = result or bishop.toBitboard() + + +proc getAttacksTo*(self: Chessboard, square: Square, attacker: PieceColor): Bitboard = + ## Computes the attack bitboard for the given square from + ## the given side + result = Bitboard(0) + 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 isOccupancyAttacked*(self: Chessboard, square: Square, occupancy: Bitboard): bool = + ## Returns whether the given square would be attacked by the + ## enemy side if the board had the given occupancy. This function + ## is necessary mostly to make sure sliding attacks can check the + ## king properly: due to how we generate our attack bitboards, if + ## the king moved backwards along a ray from a slider we would not + ## consider it to be in check (because the ray stops at the first + ## blocker). In order to fix that, in generateKingMoves() we use this + ## function and pass in the board's occupancy without the moving king so + ## that we can pick the correct magic bitboard and ray. Also, since this + ## function doesn't need to generate all the attacks to know whether a + ## given square is unsafe, it can short circuit at the first attack and + ## exit early, unlike getAttacksTo + let + sideToMove = self.position.sideToMove + nonSideToMove = sideToMove.opposite() + knights = self.getBitboard(Knight, nonSideToMove) + + # Let's do the cheap ones first (the ones which are precomputed) + if (getKnightAttacks(square) and knights) != 0: + return true + + let king = self.getBitboard(King, nonSideToMove) + + if (getKingAttacks(square) and king) != 0: + return true + + let + queens = self.getBitboard(Queen, nonSideToMove) + bishops = self.getBitboard(Bishop, nonSideToMove) or queens + + if (getBishopMoves(square, occupancy) and bishops) != 0: + return true + + let rooks = self.getBitboard(Rook, nonSideToMove) or queens + + if (getRookMoves(square, occupancy) and rooks) != 0: + return true + + # TODO: Precompute pawn moves as well? + let pawns = self.getBitboard(Pawn, nonSideToMove) + + if (self.getPawnAttacks(square, nonSideToMove) and pawns) != 0: + return true + + +proc updateChecksAndPins*(self: Chessboard) = + ## Updates internal metadata about checks and + ## pinned pieces + + # *Ahem*, stolen from https://github.com/Ciekce/voidstar/blob/424ac4624011271c4d1dbd743602c23f6dbda1de/src/position.rs + # Can you tell I'm a *great* coder? + let + sideToMove = self.position.sideToMove + nonSideToMove = sideToMove.opposite() + friendlyKing = self.getBitboard(King, sideToMove).toSquare() + friendlyPieces = self.getOccupancyFor(sideToMove) + enemyPieces = self.getOccupancyFor(nonSideToMove) + + # Update checks + self.position.checkers = self.getAttacksTo(friendlyKing, nonSideToMove) + # Update pins + self.position.diagonalPins = Bitboard(0) + self.position.orthogonalPins = Bitboard(0) + + let + diagonalAttackers = self.getBitboard(Queen, nonSideToMove) or self.getBitboard(Bishop, nonSideToMove) + orthogonalAttackers = self.getBitboard(Queen, nonSideToMove) or self.getBitboard(Rook, nonSideToMove) + canPinDiagonally = diagonalAttackers and getBishopMoves(friendlyKing, enemyPieces) + canPinOrthogonally = orthogonalAttackers and getRookMoves(friendlyKing, enemyPieces) + + for piece in canPinDiagonally: + let pinningRay = getRayBetween(friendlyKing, piece) or piece.toBitboard() + + # Is the pinning ray obstructed by any of our friendly pieces? If so, the + # piece is pinned + if (pinningRay and friendlyPieces).countSquares() > 0: + self.position.diagonalPins = self.position.diagonalPins or pinningRay + + for piece in canPinOrthogonally: + let pinningRay = getRayBetween(friendlyKing, piece) or piece.toBitboard() + if (pinningRay and friendlyPieces).countSquares() > 0: + self.position.orthogonalPins = self.position.orthogonalPins or pinningRay + + +func inCheck*(self: Chessboard): bool {.inline.} = + ## Returns if the current side to move is in check + return self.position.checkers != 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 update*(self: Chessboard) = + ## Updates the internal grid representation + ## according to the positional data stored + ## in the chessboard + for i in 0..63: + self.grid[i] = nullPiece() + for sq in self.position.pieces[White][Pawn][]: + self.grid[sq] = Piece(color: White, kind: Pawn) + for sq in self.position.pieces[Black][Pawn][]: + self.grid[sq] = Piece(color: Black, kind: Pawn) + for sq in self.position.pieces[White][Bishop][]: + self.grid[sq] = Piece(color: White, kind: Bishop) + for sq in self.position.pieces[Black][Bishop][]: + self.grid[sq] = Piece(color: Black, kind: Bishop) + for sq in self.position.pieces[White][Knight][]: + self.grid[sq] = Piece(color: White, kind: Knight) + for sq in self.position.pieces[Black][Knight][]: + self.grid[sq] = Piece(color: Black, kind: Knight) + for sq in self.position.pieces[White][Rook][]: + self.grid[sq] = Piece(color: White, kind: Rook) + for sq in self.position.pieces[Black][Rook][]: + self.grid[sq] = Piece(color: Black, kind: Rook) + for sq in self.position.pieces[White][Queen][]: + self.grid[sq] = Piece(color: White, kind: Queen) + for sq in self.position.pieces[Black][Queen][]: + self.grid[sq] = Piece(color: Black, kind: Queen) + 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 `$`*(self: Chessboard): string = + result &= "- - - - - - - -" + var file = 8 + for i in 0..7: + result &= "\n" + for j in 0..7: + let piece = self.grid[makeSquare(i, j)] + if piece.kind == Empty: + result &= "x " + continue + result &= &"{piece.toChar()} " + result &= &"{file}" + dec(file) + result &= "\n- - - - - - - -" + result &= "\na b c d e f g h" + + +proc pretty*(self: Chessboard): string = + ## Returns a colored version of the + ## board for easier visualization + var file = 8 + for i in 0..7: + if i > 0: + result &= "\n" + for j in 0..7: + # Equivalent to (i + j) mod 2 + # (I'm just evil) + if ((i + j) and 1) == 0: + result &= "\x1b[39;44;1m" + else: + result &= "\x1b[39;40;1m" + let piece = self.grid[makeSquare(i, j)] + if piece.kind == Empty: + result &= " \x1b[0m" + else: + result &= &"{piece.toPretty()} \x1b[0m" + result &= &" \x1b[33;1m{file}\x1b[0m" + dec(file) + + result &= "\n\x1b[31;1ma b c d e f g h" + result &= "\x1b[0m" + + +proc toFEN*(self: Chessboard): string = + ## Returns a FEN string of the current + ## position in the chessboard + var skip: int + # Piece placement data + for i in 0..7: + skip = 0 + for j in 0..7: + let piece = self.grid[makeSquare(i, j)] + if piece.kind == Empty: + inc(skip) + elif skip > 0: + result &= &"{skip}{piece.toChar()}" + skip = 0 + else: + result &= piece.toChar() + if skip > 0: + result &= $skip + if i < 7: + result &= "/" + result &= " " + # Active color + result &= (if self.position.sideToMove == White: "w" else: "b") + result &= " " + # Castling availability + let castleWhite = self.position.castlingAvailability.white + let castleBlack = self.position.castlingAvailability.black + if not (castleBlack.king or castleBlack.queen or castleWhite.king or castleWhite.queen): + result &= "-" + else: + if castleWhite.king: + result &= "K" + if castleWhite.queen: + result &= "Q" + if castleBlack.king: + result &= "k" + if castleBlack.queen: + result &= "q" + result &= " " + # En passant target + if self.position.enPassantSquare == nullSquare(): + result &= "-" + else: + result &= self.position.enPassantSquare.toAlgebraic() + result &= " " + # Halfmove clock + result &= $self.position.halfMoveClock + result &= " " + # Fullmove number + result &= $self.position.fullMoveCount diff --git a/Chess/nimfish/nimfishpkg/misc.nim b/Chess/nimfish/nimfishpkg/misc.nim index 40c1be3..c3dc033 100644 --- a/Chess/nimfish/nimfishpkg/misc.nim +++ b/Chess/nimfish/nimfishpkg/misc.nim @@ -1,4 +1,4 @@ -import movegen +import board import std/strformat diff --git a/Chess/nimfish/nimfishpkg/movegen.nim b/Chess/nimfish/nimfishpkg/movegen.nim index 3bed801..a7ee512 100644 --- a/Chess/nimfish/nimfishpkg/movegen.nim +++ b/Chess/nimfish/nimfishpkg/movegen.nim @@ -12,542 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -import std/strutils import std/strformat import bitboards +import board import magics import pieces import moves import position import rays +import misc -export bitboards, magics, pieces, moves, position, rays +export bitboards, magics, pieces, moves, position, rays, misc, board -type - - Chessboard* = ref object - ## A chessboard - - # The actual board where pieces live - grid: array[64, Piece] - # The current position - position*: Position - # List of all previously reached positions - positions*: seq[Position] - - -# A bunch of simple utility functions and forward declarations -proc makeMove*(self: Chessboard, move: Move): Move {.discardable.} -proc isLegal(self: Chessboard, move: Move): bool {.inline.} -proc doMove*(self: Chessboard, move: Move) -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) -proc removePiece(self: Chessboard, square: Square) -proc update*(self: Chessboard) -func inCheck*(self: Chessboard): bool {.inline.} -proc fromChar*(c: char): Piece -proc updateChecksAndPins*(self: Chessboard) - - -func kingSideRook*(color: PieceColor): Square {.inline.} = (if color == White: "h1".toSquare() else: "h8".toSquare()) -func queenSideRook*(color: PieceColor): Square {.inline.} = (if color == White: "a8".toSquare() else: "a1".toSquare()) -func longCastleKing*(color: PieceColor): Square {.inline.} = (if color == White: "c1".toSquare() else: "c8".toSquare()) -func shortCastleKing*(color: PieceColor): Square {.inline.} = (if color == White: "g1".toSquare() else: "g8".toSquare()) -func longCastleRook*(color: PieceColor): Square {.inline.} = (if color == White: "d1".toSquare() else: "d8".toSquare()) -func shortCastleRook*(color: PieceColor): Square {.inline.} = (if color == White: "f1".toSquare() else: "f8".toSquare()) - - -proc newChessboard: Chessboard = - ## Returns a new, empty chessboard - new(result) - for i in 0..63: - result.grid[i] = nullPiece() - result.position = Position(enPassantSquare: nullSquare(), sideToMove: White) - - -# Indexing operations -func `[]`(self: array[64, Piece], square: Square): Piece {.inline.} = self[square.int8] -func `[]=`(self: var array[64, Piece], square: Square, piece: Piece) {.inline.} = self[square.int8] = piece - - -func getBitboard*(self: Chessboard, kind: PieceKind, color: PieceColor): Bitboard {.inline.} = - ## Returns the positional bitboard for the given piece kind and color - return self.position.getBitboard(kind, color) - - -func getBitboard*(self: Chessboard, piece: Piece): Bitboard {.inline.} = - ## Returns the positional bitboard for the given piece type - return self.getBitboard(piece.kind, piece.color) - - -proc newChessboardFromFEN*(fen: string): Chessboard = - ## Initializes a chessboard with the - ## position encoded by the given FEN string - result = newChessboard() - var - # Current square in the grid - row: int8 = 0 - column: int8 = 0 - # Current section in the FEN string - section = 0 - # Current index into the FEN string - index = 0 - # Temporary variable to store a piece - piece: Piece - # See https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation - while index <= fen.high(): - var c = fen[index] - if c == ' ': - # Next section - inc(section) - inc(index) - continue - case section: - of 0: - # Piece placement data - case c.toLowerAscii(): - # Piece - of 'r', 'n', 'b', 'q', 'k', 'p': - let square = makeSquare(row, column) - piece = c.fromChar() - result.position.pieces[piece.color][piece.kind][].setBit(square) - result.grid[square] = piece - inc(column) - of '/': - # Next row - inc(row) - column = 0 - of '0'..'9': - # Skip x columns - let x = int(uint8(c) - uint8('0')) - if x > 8: - raise newException(ValueError, &"invalid FEN: invalid column skip size ({x} > 8)") - column += int8(x) - else: - raise newException(ValueError, &"invalid FEN: unknown piece identifier '{c}'") - of 1: - # Active color - case c: - of 'w': - result.position.sideToMove = White - of 'b': - result.position.sideToMove = Black - else: - raise newException(ValueError, &"invalid FEN: invalid active color identifier '{c}'") - of 2: - # Castling availability - case c: - # TODO - of '-': - discard - of 'K': - result.position.castlingAvailability.white.king = true - of 'Q': - result.position.castlingAvailability.white.queen = true - of 'k': - result.position.castlingAvailability.black.king = true - of 'q': - result.position.castlingAvailability.black.queen = true - else: - raise newException(ValueError, &"invalid FEN: unknown symbol '{c}' found in castlingRights availability section") - of 3: - # En passant target square - case c: - of '-': - # Field is already uninitialized to the correct state - discard - else: - result.position.enPassantSquare = fen[index..index+1].toSquare() - # Square metadata is 2 bytes long - inc(index) - of 4: - # Halfmove clock - var s = "" - while not fen[index].isSpaceAscii(): - s.add(fen[index]) - inc(index) - # Backtrack so the space is seen by the - # next iteration of the loop - dec(index) - result.position.halfMoveClock = parseInt(s).int8 - of 5: - # Fullmove number - var s = "" - while index <= fen.high(): - s.add(fen[index]) - inc(index) - result.position.fullMoveCount = parseInt(s).int8 - else: - raise newException(ValueError, "invalid FEN: too many fields in FEN string") - inc(index) - result.updateChecksAndPins() - - -proc newDefaultChessboard*: Chessboard {.inline.} = - ## Initializes a chessboard with the - ## starting position - return newChessboardFromFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") - - -func countPieces*(self: Chessboard, kind: PieceKind, color: PieceColor): int {.inline.} = - ## Returns the number of pieces with - ## the given color and type in the - ## current position - return self.position.pieces[color][kind][].countSquares() - - -func countPieces*(self: Chessboard, piece: Piece): int {.inline.} = - ## Returns the number of pieces on the board that - ## are of the same type and color as the given piece - return self.countPieces(piece.kind, piece.color) - - -func getPiece*(self: Chessboard, square: Square): Piece {.inline.} = - ## Gets the piece at the given square - return self.grid[square] - - -func getPiece*(self: Chessboard, square: string): Piece {.inline.} = - ## Gets the piece on the given square - ## in algebraic notation - return self.getPiece(square.toSquare()) - - -func getOccupancyFor(self: Chessboard, color: PieceColor): Bitboard = - ## Get the occupancy bitboard for every piece of the given color - result = Bitboard(0) - for b in self.position.pieces[color][]: - result = result or b - - -func getOccupancy(self: Chessboard): Bitboard {.inline.} = - ## Get the occupancy bitboard for every piece on - ## the chessboard - result = self.getOccupancyFor(Black) or self.getOccupancyFor(White) - - -func getPawnAttacks(self: Chessboard, square: Square, attacker: PieceColor): Bitboard {.inline.} = - ## Returns the locations of the pawns attacking the given square - let - sq = square.toBitboard() - pawns = self.getBitboard(Pawn, attacker) - bottomLeft = sq.backwardLeftRelativeTo(attacker) - bottomRight = sq.backwardRightRelativeTo(attacker) - return pawns and (bottomLeft or bottomRight) - - -func getKingAttacks(self: Chessboard, square: Square, attacker: PieceColor): Bitboard {.inline.} = - ## Returns the location of the king if it is attacking the given square - result = Bitboard(0) - let - king = self.getBitboard(King, attacker) - if (getKingAttacks(square) and king) != 0: - result = result or king - - -func getKnightAttacks(self: Chessboard, square: Square, attacker: PieceColor): Bitboard = - ## Returns the locations of the knights attacking the given square - let - knights = self.getBitboard(Knight, attacker) - result = Bitboard(0) - for knight in knights: - let knightBB = knight.toBitboard() - if (getKnightAttacks(knight) and knightBB) != 0: - result = result or knightBB - - -proc getSlidingAttacks(self: Chessboard, square: Square, attacker: PieceColor): Bitboard = - ## Returns the locations of the sliding pieces attacking the given square - let - queens = self.getBitboard(Queen, attacker) - rooks = self.getBitboard(Rook, attacker) or queens - bishops = self.getBitboard(Bishop, attacker) or queens - occupancy = self.getOccupancy() - squareBB = square.toBitboard() - result = Bitboard(0) - for rook in rooks: - let - blockers = occupancy and Rook.getRelevantBlockers(rook) - moves = getRookMoves(rook, blockers) - # Attack set intersects our chosen square - if (moves and squareBB) != 0: - result = result or rook.toBitboard() - for bishop in bishops: - let - blockers = occupancy and Bishop.getRelevantBlockers(bishop) - moves = getBishopMoves(bishop, blockers) - if (moves and squareBB) != 0: - result = result or bishop.toBitboard() - - -proc getAttacksTo*(self: Chessboard, square: Square, attacker: PieceColor): Bitboard = - ## Computes the attack bitboard for the given square from - ## the given side - result = Bitboard(0) - 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 isOccupancyAttacked*(self: Chessboard, square: Square, occupancy: Bitboard): bool = - ## Returns whether the given square would be attacked by the - ## enemy side if the board had the given occupancy. This function - ## is necessary mostly to make sure sliding attacks can check the - ## king properly: due to how we generate our attack bitboards, if - ## the king moved backwards along a ray from a slider we would not - ## consider it to be in check (because the ray stops at the first - ## blocker). In order to fix that, in generateKingMoves() we use this - ## function and pass in the board's occupancy without the moving king so - ## that we can pick the correct magic bitboard and ray. Also, since this - ## function doesn't need to generate all the attacks to know whether a - ## given square is unsafe, it can short circuit at the first attack and - ## exit early, unlike getAttacksTo - let - sideToMove = self.position.sideToMove - nonSideToMove = sideToMove.opposite() - knights = self.getBitboard(Knight, nonSideToMove) - - # Let's do the cheap ones first (the ones which are precomputed) - if (getKnightAttacks(square) and knights) != 0: - return true - - let king = self.getBitboard(King, nonSideToMove) - - if (getKingAttacks(square) and king) != 0: - return true - - let - queens = self.getBitboard(Queen, nonSideToMove) - bishops = self.getBitboard(Bishop, nonSideToMove) or queens - - if (getBishopMoves(square, occupancy) and bishops) != 0: - return true - - let rooks = self.getBitboard(Rook, nonSideToMove) or queens - - if (getRookMoves(square, occupancy) and rooks) != 0: - return true - - # TODO: Precompute pawn moves as well? - let pawns = self.getBitboard(Pawn, nonSideToMove) - - if (self.getPawnAttacks(square, nonSideToMove) and pawns) != 0: - return true - - -proc updateChecksAndPins*(self: Chessboard) = - ## Updates internal metadata about checks and - ## pinned pieces - - # *Ahem*, stolen from https://github.com/Ciekce/voidstar/blob/424ac4624011271c4d1dbd743602c23f6dbda1de/src/position.rs - # Can you tell I'm a *great* coder? - let - sideToMove = self.position.sideToMove - nonSideToMove = sideToMove.opposite() - friendlyKing = self.getBitboard(King, sideToMove).toSquare() - friendlyPieces = self.getOccupancyFor(sideToMove) - enemyPieces = self.getOccupancyFor(nonSideToMove) - - # Update checks - self.position.checkers = self.getAttacksTo(friendlyKing, nonSideToMove) - # Update pins - self.position.diagonalPins = Bitboard(0) - self.position.orthogonalPins = Bitboard(0) - - let - diagonalAttackers = self.getBitboard(Queen, nonSideToMove) or self.getBitboard(Bishop, nonSideToMove) - orthogonalAttackers = self.getBitboard(Queen, nonSideToMove) or self.getBitboard(Rook, nonSideToMove) - canPinDiagonally = diagonalAttackers and getBishopMoves(friendlyKing, enemyPieces) - canPinOrthogonally = orthogonalAttackers and getRookMoves(friendlyKing, enemyPieces) - - for piece in canPinDiagonally: - let pinningRay = getRayBetween(friendlyKing, piece) or piece.toBitboard() - - # Is the pinning ray obstructed by any of our friendly pieces? If so, the - # piece is pinned - if (pinningRay and friendlyPieces).countSquares() > 0: - self.position.diagonalPins = self.position.diagonalPins or pinningRay - - for piece in canPinOrthogonally: - let pinningRay = getRayBetween(friendlyKing, piece) or piece.toBitboard() - if (pinningRay and friendlyPieces).countSquares() > 0: - self.position.orthogonalPins = self.position.orthogonalPins or pinningRay - - -func inCheck(self: Chessboard): bool {.inline.} = - ## Returns if the current side to move is in check - return self.position.checkers != 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 removePieceFromBitboard(self: Chessboard, square: Square) = - ## Removes a piece at the given square in the chessboard from - ## its respective bitboard - let piece = self.grid[square] - self.position.pieces[piece.color][piece.kind][].clearBit(square) - - -proc addPieceToBitboard(self: Chessboard, square: Square, piece: Piece) = - ## Adds the given piece at the given square in the chessboard to - ## its respective bitboard - self.position.pieces[piece.color][piece.kind][].setBit(square) - - -proc spawnPiece(self: Chessboard, square: Square, piece: Piece) = - ## Internal helper to "spawn" a given piece at the given - ## square - when not defined(danger): - doAssert self.grid[square].kind == Empty - self.addPieceToBitboard(square, piece) - self.grid[square] = piece - - -proc removePiece(self: Chessboard, square: Square) = - ## Removes a piece from the board, updating necessary - ## metadata - var piece = self.grid[square] - when not defined(danger): - doAssert piece.kind != Empty and piece.color != None, self.toFEN() - self.removePieceFromBitboard(square) - self.grid[square] = nullPiece() - - -proc movePiece(self: Chessboard, move: Move) = - ## Internal helper to move a piece from - ## its current square to a target square - let piece = self.grid[move.startSquare] - when not defined(danger): - let targetSquare = self.getPiece(move.targetSquare) - if targetSquare.color != None: - raise newException(AccessViolationDefect, &"{piece} at {move.startSquare} attempted to overwrite {targetSquare} at {move.targetSquare}: {move}") - # Update positional metadata - self.removePiece(move.startSquare) - self.spawnPiece(move.targetSquare, piece) - - -proc doMove*(self: Chessboard, move: Move) = - ## Internal function called by makeMove after - ## performing legality checks. Can be used in - ## performance-critical paths where a move is - ## already known to be legal (i.e. during search) - - # Record final position for future reference - self.positions.add(self.position) - - # Final checks - let piece = self.grid[move.startSquare] - when not defined(danger): - doAssert piece.kind != Empty and piece.color != None, &"{move} {self.toFEN()}" - - var - halfMoveClock = self.position.halfMoveClock - fullMoveCount = self.position.fullMoveCount - 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(): - # Number of half-moves since the last reversible half-move - halfMoveClock = 0 - else: - inc(halfMoveClock) - if piece.color == Black: - inc(fullMoveCount) - - if move.isDoublePush(): - enPassantTarget = move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare() - - # Create new position - self.position = Position(plyFromRoot: self.position.plyFromRoot + 1, - halfMoveClock: halfMoveClock, - fullMoveCount: fullMoveCount, - sideToMove: self.position.sideToMove.opposite(), - castlingRights: castlingRights, - enPassantSquare: enPassantTarget, - pieces: self.position.pieces - ) - # Update position metadata - - if move.isEnPassant(): - # Make the en passant pawn disappear - self.removePiece(move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare()) - - if move.isCapture(): - # Get rid of captured pieces - self.removePiece(move.targetSquare) - - # Move the piece to its target square - self.movePiece(move) - # TODO: Castling! - if move.isPromotion(): - # Move is a pawn promotion: get rid of the pawn - # and spawn a new piece - self.removePiece(move.targetSquare) - case move.getPromotionType(): - of PromoteToBishop: - self.spawnPiece(move.targetSquare, Piece(kind: Bishop, color: piece.color)) - of PromoteToKnight: - self.spawnPiece(move.targetSquare, Piece(kind: Knight, color: piece.color)) - of PromoteToRook: - self.spawnPiece(move.targetSquare, Piece(kind: Rook, color: piece.color)) - of PromoteToQueen: - self.spawnPiece(move.targetSquare, Piece(kind: Queen, color: piece.color)) - else: - # Unreachable - discard - # Updates checks and pins for the side to move - self.updateChecksAndPins() - - -proc update*(self: Chessboard) = - ## Updates the internal grid representation - ## according to the positional data stored - ## in the chessboard - for i in 0..63: - self.grid[i] = nullPiece() - for sq in self.position.pieces[White][Pawn][]: - self.grid[sq] = Piece(color: White, kind: Pawn) - for sq in self.position.pieces[Black][Pawn][]: - self.grid[sq] = Piece(color: Black, kind: Pawn) - for sq in self.position.pieces[White][Bishop][]: - self.grid[sq] = Piece(color: White, kind: Bishop) - for sq in self.position.pieces[Black][Bishop][]: - self.grid[sq] = Piece(color: Black, kind: Bishop) - for sq in self.position.pieces[White][Knight][]: - self.grid[sq] = Piece(color: White, kind: Knight) - for sq in self.position.pieces[Black][Knight][]: - self.grid[sq] = Piece(color: Black, kind: Knight) - for sq in self.position.pieces[White][Rook][]: - self.grid[sq] = Piece(color: White, kind: Rook) - for sq in self.position.pieces[Black][Rook][]: - self.grid[sq] = Piece(color: Black, kind: Rook) - for sq in self.position.pieces[White][Queen][]: - self.grid[sq] = Piece(color: White, kind: Queen) - for sq in self.position.pieces[Black][Queen][]: - self.grid[sq] = Piece(color: Black, kind: Queen) - 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 - self.position = self.positions.pop() - self.update() - proc generatePawnMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) = let @@ -726,7 +206,126 @@ proc generateMoves*(self: Chessboard, moves: var MoveList) = # Queens are just handled rooks + bishops -proc isLegal(self: Chessboard, move: Move): bool {.inline.} = + +proc removePieceFromBitboard(self: Chessboard, square: Square) = + ## Removes a piece at the given square in the chessboard from + ## its respective bitboard + let piece = self.grid[square] + self.position.pieces[piece.color][piece.kind][].clearBit(square) + + +proc addPieceToBitboard(self: Chessboard, square: Square, piece: Piece) = + ## Adds the given piece at the given square in the chessboard to + ## its respective bitboard + self.position.pieces[piece.color][piece.kind][].setBit(square) + + +proc spawnPiece(self: Chessboard, square: Square, piece: Piece) = + ## Internal helper to "spawn" a given piece at the given + ## square + when not defined(danger): + doAssert self.grid[square].kind == Empty + self.addPieceToBitboard(square, piece) + self.grid[square] = piece + + +proc removePiece(self: Chessboard, square: Square) = + ## Removes a piece from the board, updating necessary + ## metadata + var piece = self.grid[square] + when not defined(danger): + doAssert piece.kind != Empty and piece.color != None, self.toFEN() + self.removePieceFromBitboard(square) + self.grid[square] = nullPiece() + + +proc movePiece(self: Chessboard, move: Move) = + ## Internal helper to move a piece from + ## its current square to a target square + let piece = self.grid[move.startSquare] + when not defined(danger): + let targetSquare = self.getPiece(move.targetSquare) + if targetSquare.color != None: + raise newException(AccessViolationDefect, &"{piece} at {move.startSquare} attempted to overwrite {targetSquare} at {move.targetSquare}: {move}") + # Update positional metadata + self.removePiece(move.startSquare) + self.spawnPiece(move.targetSquare, piece) + + +proc doMove*(self: Chessboard, move: Move) = + ## Internal function called by makeMove after + ## performing legality checks. Can be used in + ## performance-critical paths where a move is + ## already known to be legal (i.e. during search) + + # Record final position for future reference + self.positions.add(self.position) + + # Final checks + let piece = self.grid[move.startSquare] + when not defined(danger): + doAssert piece.kind != Empty and piece.color != None, &"{move} {self.toFEN()}" + + var + halfMoveClock = self.position.halfMoveClock + fullMoveCount = self.position.fullMoveCount + 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(): + # Number of half-moves since the last reversible half-move + halfMoveClock = 0 + else: + inc(halfMoveClock) + if piece.color == Black: + inc(fullMoveCount) + + if move.isDoublePush(): + enPassantTarget = move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare() + + # Create new position + self.position = Position(plyFromRoot: self.position.plyFromRoot + 1, + halfMoveClock: halfMoveClock, + fullMoveCount: fullMoveCount, + sideToMove: self.position.sideToMove.opposite(), + castlingRights: castlingRights, + enPassantSquare: enPassantTarget, + pieces: self.position.pieces + ) + # Update position metadata + + if move.isEnPassant(): + # Make the en passant pawn disappear + self.removePiece(move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare()) + + if move.isCapture(): + # Get rid of captured pieces + self.removePiece(move.targetSquare) + + # Move the piece to its target square + self.movePiece(move) + # TODO: Castling! + if move.isPromotion(): + # Move is a pawn promotion: get rid of the pawn + # and spawn a new piece + self.removePiece(move.targetSquare) + case move.getPromotionType(): + of PromoteToBishop: + self.spawnPiece(move.targetSquare, Piece(kind: Bishop, color: piece.color)) + of PromoteToKnight: + self.spawnPiece(move.targetSquare, Piece(kind: Knight, color: piece.color)) + of PromoteToRook: + self.spawnPiece(move.targetSquare, Piece(kind: Rook, color: piece.color)) + of PromoteToQueen: + self.spawnPiece(move.targetSquare, Piece(kind: Queen, color: piece.color)) + else: + # Unreachable + discard + # Updates checks and pins for the side to move + self.updateChecksAndPins() + + +proc isLegal*(self: Chessboard, move: Move): bool {.inline.} = ## Returns whether the given move is legal var moves = MoveList() self.generateMoves(moves) @@ -736,183 +335,15 @@ proc isLegal(self: Chessboard, move: Move): bool {.inline.} = proc makeMove*(self: Chessboard, move: Move): Move {.discardable.} = ## Makes a move on the board result = move + # Updates checks and pins for the side to move + self.updateChecksAndPins() if not self.isLegal(move): return nullMove() self.doMove(move) -proc toChar*(piece: Piece): char = - case piece.kind: - of Bishop: - result = 'b' - of King: - result = 'k' - of Knight: - result = 'n' - of Pawn: - result = 'p' - of Queen: - result = 'q' - of Rook: - result = 'r' - else: - discard - if piece.color == White: - result = result.toUpperAscii() - - -proc fromChar*(c: char): Piece = - var - kind: PieceKind - color = Black - case c.toLowerAscii(): - of 'b': - kind = Bishop - of 'k': - kind = King - of 'n': - kind = Knight - of 'p': - kind = Pawn - of 'q': - kind = Queen - of 'r': - kind = Rook - else: - discard - if c.isUpperAscii(): - color = White - result = Piece(kind: kind, color: color) - - -proc `$`*(self: Chessboard): string = - result &= "- - - - - - - -" - var file = 8 - for i in 0..7: - result &= "\n" - for j in 0..7: - let piece = self.grid[makeSquare(i, j)] - if piece.kind == Empty: - result &= "x " - continue - result &= &"{piece.toChar()} " - result &= &"{file}" - dec(file) - result &= "\n- - - - - - - -" - result &= "\na b c d e f g h" - - -proc toPretty*(piece: Piece): string = - case piece.color: - of White: - case piece.kind: - of King: - return "\U2654" - of Queen: - return "\U2655" - of Rook: - return "\U2656" - of Bishop: - return "\U2657" - of Knight: - return "\U2658" - of Pawn: - return "\U2659" - else: - discard - of Black: - case piece.kind: - of King: - return "\U265A" - of Queen: - return "\U265B" - of Rook: - return "\U265C" - of Bishop: - return "\U265D" - of Knight: - return "\U265E" - of Pawn: - return "\240\159\168\133" - else: - discard - else: - discard - - -proc pretty*(self: Chessboard): string = - ## Returns a colored version of the - ## board for easier visualization - var file = 8 - for i in 0..7: - if i > 0: - result &= "\n" - for j in 0..7: - # Equivalent to (i + j) mod 2 - # (I'm just evil) - if ((i + j) and 1) == 0: - result &= "\x1b[39;44;1m" - else: - result &= "\x1b[39;40;1m" - let piece = self.grid[makeSquare(i, j)] - if piece.kind == Empty: - result &= " \x1b[0m" - else: - result &= &"{piece.toPretty()} \x1b[0m" - result &= &" \x1b[33;1m{file}\x1b[0m" - dec(file) - - result &= "\n\x1b[31;1ma b c d e f g h" - result &= "\x1b[0m" - - -proc toFEN*(self: Chessboard): string = - ## Returns a FEN string of the current - ## position in the chessboard - var skip: int - # Piece placement data - for i in 0..7: - skip = 0 - for j in 0..7: - let piece = self.grid[makeSquare(i, j)] - if piece.kind == Empty: - inc(skip) - elif skip > 0: - result &= &"{skip}{piece.toChar()}" - skip = 0 - else: - result &= piece.toChar() - if skip > 0: - result &= $skip - if i < 7: - result &= "/" - result &= " " - # Active color - result &= (if self.position.sideToMove == White: "w" else: "b") - result &= " " - # Castling availability - let castleWhite = self.position.castlingAvailability.white - let castleBlack = self.position.castlingAvailability.black - if not (castleBlack.king or castleBlack.queen or castleWhite.king or castleWhite.queen): - result &= "-" - else: - if castleWhite.king: - result &= "K" - if castleWhite.queen: - result &= "Q" - if castleBlack.king: - result &= "k" - if castleBlack.queen: - result &= "q" - result &= " " - # En passant target - if self.position.enPassantSquare == nullSquare(): - result &= "-" - else: - result &= self.position.enPassantSquare.toAlgebraic() - result &= " " - # Halfmove clock - result &= $self.position.halfMoveClock - result &= " " - # Fullmove number - result &= $self.position.fullMoveCount +proc unmakeMove*(self: Chessboard) = + ## Reverts to the previous board position, + ## if one exists + self.position = self.positions.pop() + self.update() diff --git a/Chess/nimfish/nimfishpkg/pieces.nim b/Chess/nimfish/nimfishpkg/pieces.nim index 8b503b4..2807275 100644 --- a/Chess/nimfish/nimfishpkg/pieces.nim +++ b/Chess/nimfish/nimfishpkg/pieces.nim @@ -76,4 +76,93 @@ proc toAlgebraic*(square: Square): string {.inline.} = return &"{file}{rank}" -proc `$`*(square: Square): string = square.toAlgebraic() \ No newline at end of file +proc `$`*(square: Square): string = square.toAlgebraic() + +func kingSideRook*(color: PieceColor): Square {.inline.} = (if color == White: "h1".toSquare() else: "h8".toSquare()) +func queenSideRook*(color: PieceColor): Square {.inline.} = (if color == White: "a8".toSquare() else: "a1".toSquare()) +func longCastleKing*(color: PieceColor): Square {.inline.} = (if color == White: "c1".toSquare() else: "c8".toSquare()) +func shortCastleKing*(color: PieceColor): Square {.inline.} = (if color == White: "g1".toSquare() else: "g8".toSquare()) +func longCastleRook*(color: PieceColor): Square {.inline.} = (if color == White: "d1".toSquare() else: "d8".toSquare()) +func shortCastleRook*(color: PieceColor): Square {.inline.} = (if color == White: "f1".toSquare() else: "f8".toSquare()) + + +proc toPretty*(piece: Piece): string = + case piece.color: + of White: + case piece.kind: + of King: + return "\U2654" + of Queen: + return "\U2655" + of Rook: + return "\U2656" + of Bishop: + return "\U2657" + of Knight: + return "\U2658" + of Pawn: + return "\U2659" + else: + discard + of Black: + case piece.kind: + of King: + return "\U265A" + of Queen: + return "\U265B" + of Rook: + return "\U265C" + of Bishop: + return "\U265D" + of Knight: + return "\U265E" + of Pawn: + return "\240\159\168\133" + else: + discard + else: + discard + + +func toChar*(piece: Piece): char = + case piece.kind: + of Bishop: + result = 'b' + of King: + result = 'k' + of Knight: + result = 'n' + of Pawn: + result = 'p' + of Queen: + result = 'q' + of Rook: + result = 'r' + else: + discard + if piece.color == White: + result = result.toUpperAscii() + + +func fromChar*(c: char): Piece = + var + kind: PieceKind + color = Black + case c.toLowerAscii(): + of 'b': + kind = Bishop + of 'k': + kind = King + of 'n': + kind = Knight + of 'p': + kind = Pawn + of 'q': + kind = Queen + of 'r': + kind = Rook + else: + discard + if c.isUpperAscii(): + color = White + result = Piece(kind: kind, color: color) \ No newline at end of file