diff --git a/Chess/nimfish/nimfish.nim b/Chess/nimfish/nimfish.nim index d97d335..3f2e173 100644 --- a/Chess/nimfish/nimfish.nim +++ b/Chess/nimfish/nimfish.nim @@ -1,929 +1,20 @@ -# Copyright 2023 Mattia Giambirtone & All Contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import std/strutils -import std/strformat - - +import nimfishpkg/tui +import nimfishpkg/misc +import nimfishpkg/movegen import nimfishpkg/bitboards -import nimfishpkg/magics -import nimfishpkg/pieces import nimfishpkg/moves -import nimfishpkg/position +import nimfishpkg/pieces +import nimfishpkg/magics import nimfishpkg/rays +import nimfishpkg/position -export bitboards, magics, pieces, moves, position - - -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 - sideToMove = self.position.sideToMove - pawns = self.getBitboard(Pawn, sideToMove) - occupancy = self.getOccupancy() - # We can only capture enemy pieces (except the king) - enemyPieces = self.getOccupancyFor(sideToMove.opposite()) - # We can only capture diagonally and forward - rightMovement = pawns.forwardRightRelativeTo(sideToMove) - leftMovement = pawns.forwardLeftRelativeTo(sideToMove) - epTarget = self.position.enPassantSquare - checkers = self.position.checkers - diagonalPins = self.position.diagonalPins - orthogonalPins = self.position.orthogonalPins - promotionRank = if sideToMove == White: getRankMask(0) else: getRankMask(7) - # The rank where each color's side starts - # TODO: Give names to ranks and files so we don't have to assume a - # specific board layout when calling get(Rank|File)Mask - startingRank = if sideToMove == White: getRankMask(6) else: getRankMask(1) - - var epBitboard = if epTarget != nullSquare(): epTarget.toBitboard() else: Bitboard(0) - let epPawn = if epBitboard == 0: Bitboard(0) else: epBitboard.forwardRelativeTo(sideToMove) - # If we are in check, en passant is only possible if we'd capture the (only) - # checking pawn with it - if epBitboard != 0 and self.inCheck() and (epPawn and checkers).countSquares() == 0: - epBitboard = Bitboard(0) - - # Single and double pushes - let - # If a pawn is pinned diagonally, it cannot move - pushablePawns = pawns and not diagonalPins - # Neither can it move if it's pinned orthogonally - singlePushes = pushablePawns.forwardRelativeTo(sideToMove) and not occupancy and not orthogonalPins - # Only pawns on their starting rank can double push - doublePushes = (pushablePawns and startingRank).doubleForwardRelativeTo(sideToMove) and not occupancy and orthogonalPins - - -proc generateRookMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) = - ## Helper of generateSlidingMoves to generate rook moves - let - sideToMove = self.position.sideToMove - occupancy = self.getOccupancy() - enemyPieces = self.getOccupancyFor(sideToMove.opposite()) - rooks = self.getBitboard(Rook, sideToMove) - queens = self.getBitboard(Queen, sideToMove) - movableRooks = not self.position.diagonalPins and (queens or rooks) - pinMask = self.position.orthogonalPins - pinnedRooks = movableRooks and pinMask - unpinnedRooks = movableRooks and not pinnedRooks - for square in pinnedRooks: - let - blockers = occupancy and Rook.getRelevantBlockers(square) - moveset = getRookMoves(square, blockers) - for target in moveset and not occupancy and pinMask and mask: - moves.add(createMove(square, target)) - for target in moveset and enemyPieces and pinMask and mask: - moves.add(createMove(square, target, Capture)) - for square in unpinnedRooks: - let - blockers = occupancy and Rook.getRelevantBlockers(square) - moveset = getRookMoves(square, blockers) - for target in moveset and not occupancy and mask: - moves.add(createMove(square, target)) - for target in moveset and enemyPieces and mask: - moves.add(createMove(square, target, Capture)) - - -proc generateBishopMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) = - ## Helper of generateSlidingMoves to generate bishop moves - let - sideToMove = self.position.sideToMove - occupancy = self.getOccupancy() - enemyPieces = self.getOccupancyFor(sideToMove.opposite()) - bishops = self.getBitboard(Bishop, sideToMove) - queens = self.getBitboard(Queen, sideToMove) - movableBishops = not self.position.orthogonalPins and (queens or bishops) - pinMask = self.position.diagonalPins - pinnedBishops = movableBishops and pinMask - unpinnedBishops = movableBishops and not pinnedBishops - for square in pinnedBishops: - let - blockers = occupancy and Bishop.getRelevantBlockers(square) - moveset = getBishopMoves(square, blockers) - for target in moveset and pinMask and mask: - moves.add(createMove(square, target)) - for target in moveset and enemyPieces and pinMask and mask: - moves.add(createMove(square, target, Capture)) - for square in unpinnedBishops: - let - blockers = occupancy and Bishop.getRelevantBlockers(square) - moveset = getBishopMoves(square, blockers) - for target in moveset and mask: - moves.add(createMove(square, target)) - for target in moveset and enemyPieces and mask: - moves.add(createMove(square, target, Capture)) - - -proc generateKingMoves(self: Chessboard, moves: var MoveList) = - ## Generates all legal king moves for the side to move - let - sideToMove = self.position.sideToMove - king = self.getBitboard(King, sideToMove) - occupancy = self.getOccupancy() - nonSideToMove = sideToMove.opposite() - enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove) - bitboard = getKingAttacks(king.toSquare()) - noKingOccupancy = occupancy and not king - for square in bitboard and not occupancy: - if not self.isOccupancyAttacked(square, noKingOccupancy): - moves.add(createMove(king, square)) - for square in bitboard and enemyPieces: - if not self.isOccupancyAttacked(square, noKingOccupancy): - moves.add(createMove(king, square, Capture)) - - -proc generateKnightMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) = - ## Generates all the legal knight moves for the side to move - let - sideToMove = self.position.sideToMove - knights = self.getBitboard(Knight, sideToMove) - nonSideToMove = sideToMove.opposite() - pinned = self.position.diagonalPins or self.position.orthogonalPins - unpinnedKnights = knights and not pinned - enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove) - for square in unpinnedKnights: - let bitboard = getKnightAttacks(square) - for target in bitboard and mask: - moves.add(createMove(square, target)) - for target in bitboard and enemyPieces: - moves.add(createMove(square, target, Capture)) - - -proc generateMoves*(self: Chessboard, moves: var MoveList) = - ## Generates the list of all possible legal moves - ## in the current position - if self.position.halfMoveClock >= 100: - # Draw by 50-move rule - return - let sideToMove = self.position.sideToMove - # TODO: Check for draw by insufficient material - # TODO: Check for repetitions (requires zobrist hashing + table) - self.generateKingMoves(moves) - if self.position.checkers.countSquares() > 1: - # King is in double check: no need to generate any more - # moves - return - if not self.inCheck(): - # TODO: Castling - discard - - # We pass a mask to our move generators to remove stuff - # like our friendly pieces from the set of possible - # target squares, as well as to ensure checks are not - # ignored - - var mask: Bitboard - if not self.inCheck(): - # Not in check: cannot move over friendly pieces - mask = not self.getOccupancyFor(sideToMove) - else: - # We *are* in check (from a single piece, because the two checks - # case was handled above already). If the piece is a slider, we'll - # extract the ray from it to our king and add the checking piece to - # it, meaning the only legal moves are those that either block the - # check or capture the checking piece. For other non-sliding pieces - # the ray will be empty so the only legal move will be to capture - # the checking piece - let checker = self.position.checkers.lowestSquare() - mask = getRayBetween(checker, self.getBitboard(King, sideToMove).toSquare()) or checker.toBitboard() - - self.generatePawnMoves(moves, mask) - self.generateKnightMoves(moves, mask) - self.generateRookMoves(moves, mask) - self.generateBishopMoves(moves, mask) - # Queens are just handled rooks + bishops - - -proc isLegal(self: Chessboard, move: Move): bool {.inline.} = - ## Returns whether the given move is legal - var moves = MoveList() - self.generateMoves(moves) - return move in moves - - -proc makeMove*(self: Chessboard, move: Move): Move {.discardable.} = - ## Makes a move on the board - result = move - 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 +export tui, misc, movegen, bitboards, moves, pieces, magics, rays, position when isMainModule: - import nimfishpkg/tui - import nimfishpkg/misc - basicTests() setControlCHook(proc () {.noconv.} = quit(0)) - quit(commandLoop()) + quit(commandLoop()) \ No newline at end of file diff --git a/Chess/nimfish/nimfishpkg/misc.nim b/Chess/nimfish/nimfishpkg/misc.nim index 16b91e8..40c1be3 100644 --- a/Chess/nimfish/nimfishpkg/misc.nim +++ b/Chess/nimfish/nimfishpkg/misc.nim @@ -1,4 +1,4 @@ -import ../nimfish +import movegen import std/strformat diff --git a/Chess/nimfish/nimfishpkg/movegen.nim b/Chess/nimfish/nimfishpkg/movegen.nim new file mode 100644 index 0000000..3bed801 --- /dev/null +++ b/Chess/nimfish/nimfishpkg/movegen.nim @@ -0,0 +1,918 @@ +# Copyright 2023 Mattia Giambirtone & All Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import std/strutils +import std/strformat + + +import bitboards +import magics +import pieces +import moves +import position +import rays + + +export bitboards, magics, pieces, moves, position, 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 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 + sideToMove = self.position.sideToMove + pawns = self.getBitboard(Pawn, sideToMove) + occupancy = self.getOccupancy() + # We can only capture enemy pieces (except the king) + enemyPieces = self.getOccupancyFor(sideToMove.opposite()) + # We can only capture diagonally and forward + rightMovement = pawns.forwardRightRelativeTo(sideToMove) + leftMovement = pawns.forwardLeftRelativeTo(sideToMove) + epTarget = self.position.enPassantSquare + checkers = self.position.checkers + diagonalPins = self.position.diagonalPins + orthogonalPins = self.position.orthogonalPins + promotionRank = if sideToMove == White: getRankMask(0) else: getRankMask(7) + # The rank where each color's side starts + # TODO: Give names to ranks and files so we don't have to assume a + # specific board layout when calling get(Rank|File)Mask + startingRank = if sideToMove == White: getRankMask(6) else: getRankMask(1) + + var epBitboard = if epTarget != nullSquare(): epTarget.toBitboard() else: Bitboard(0) + let epPawn = if epBitboard == 0: Bitboard(0) else: epBitboard.forwardRelativeTo(sideToMove) + # If we are in check, en passant is only possible if we'd capture the (only) + # checking pawn with it + if epBitboard != 0 and self.inCheck() and (epPawn and checkers).countSquares() == 0: + epBitboard = Bitboard(0) + + # Single and double pushes + let + # If a pawn is pinned diagonally, it cannot move + pushablePawns = pawns and not diagonalPins + # Neither can it move if it's pinned orthogonally + singlePushes = pushablePawns.forwardRelativeTo(sideToMove) and not occupancy and not orthogonalPins + # Only pawns on their starting rank can double push + doublePushes = (pushablePawns and startingRank).doubleForwardRelativeTo(sideToMove) and not occupancy and orthogonalPins + + +proc generateRookMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) = + ## Helper of generateSlidingMoves to generate rook moves + let + sideToMove = self.position.sideToMove + occupancy = self.getOccupancy() + enemyPieces = self.getOccupancyFor(sideToMove.opposite()) + rooks = self.getBitboard(Rook, sideToMove) + queens = self.getBitboard(Queen, sideToMove) + movableRooks = not self.position.diagonalPins and (queens or rooks) + pinMask = self.position.orthogonalPins + pinnedRooks = movableRooks and pinMask + unpinnedRooks = movableRooks and not pinnedRooks + for square in pinnedRooks: + let + blockers = occupancy and Rook.getRelevantBlockers(square) + moveset = getRookMoves(square, blockers) + for target in moveset and not occupancy and pinMask and mask: + moves.add(createMove(square, target)) + for target in moveset and enemyPieces and pinMask and mask: + moves.add(createMove(square, target, Capture)) + for square in unpinnedRooks: + let + blockers = occupancy and Rook.getRelevantBlockers(square) + moveset = getRookMoves(square, blockers) + for target in moveset and not occupancy and mask: + moves.add(createMove(square, target)) + for target in moveset and enemyPieces and mask: + moves.add(createMove(square, target, Capture)) + + +proc generateBishopMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) = + ## Helper of generateSlidingMoves to generate bishop moves + let + sideToMove = self.position.sideToMove + occupancy = self.getOccupancy() + enemyPieces = self.getOccupancyFor(sideToMove.opposite()) + bishops = self.getBitboard(Bishop, sideToMove) + queens = self.getBitboard(Queen, sideToMove) + movableBishops = not self.position.orthogonalPins and (queens or bishops) + pinMask = self.position.diagonalPins + pinnedBishops = movableBishops and pinMask + unpinnedBishops = movableBishops and not pinnedBishops + for square in pinnedBishops: + let + blockers = occupancy and Bishop.getRelevantBlockers(square) + moveset = getBishopMoves(square, blockers) + for target in moveset and pinMask and mask: + moves.add(createMove(square, target)) + for target in moveset and enemyPieces and pinMask and mask: + moves.add(createMove(square, target, Capture)) + for square in unpinnedBishops: + let + blockers = occupancy and Bishop.getRelevantBlockers(square) + moveset = getBishopMoves(square, blockers) + for target in moveset and mask: + moves.add(createMove(square, target)) + for target in moveset and enemyPieces and mask: + moves.add(createMove(square, target, Capture)) + + +proc generateKingMoves(self: Chessboard, moves: var MoveList) = + ## Generates all legal king moves for the side to move + let + sideToMove = self.position.sideToMove + king = self.getBitboard(King, sideToMove) + occupancy = self.getOccupancy() + nonSideToMove = sideToMove.opposite() + enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove) + bitboard = getKingAttacks(king.toSquare()) + noKingOccupancy = occupancy and not king + for square in bitboard and not occupancy: + if not self.isOccupancyAttacked(square, noKingOccupancy): + moves.add(createMove(king, square)) + for square in bitboard and enemyPieces: + if not self.isOccupancyAttacked(square, noKingOccupancy): + moves.add(createMove(king, square, Capture)) + + +proc generateKnightMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) = + ## Generates all the legal knight moves for the side to move + let + sideToMove = self.position.sideToMove + knights = self.getBitboard(Knight, sideToMove) + nonSideToMove = sideToMove.opposite() + pinned = self.position.diagonalPins or self.position.orthogonalPins + unpinnedKnights = knights and not pinned + enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove) + for square in unpinnedKnights: + let bitboard = getKnightAttacks(square) + for target in bitboard and mask: + moves.add(createMove(square, target)) + for target in bitboard and enemyPieces: + moves.add(createMove(square, target, Capture)) + + +proc generateMoves*(self: Chessboard, moves: var MoveList) = + ## Generates the list of all possible legal moves + ## in the current position + if self.position.halfMoveClock >= 100: + # Draw by 50-move rule + return + let sideToMove = self.position.sideToMove + # TODO: Check for draw by insufficient material + # TODO: Check for repetitions (requires zobrist hashing + table) + self.generateKingMoves(moves) + if self.position.checkers.countSquares() > 1: + # King is in double check: no need to generate any more + # moves + return + if not self.inCheck(): + # TODO: Castling + discard + + # We pass a mask to our move generators to remove stuff + # like our friendly pieces from the set of possible + # target squares, as well as to ensure checks are not + # ignored + + var mask: Bitboard + if not self.inCheck(): + # Not in check: cannot move over friendly pieces + mask = not self.getOccupancyFor(sideToMove) + else: + # We *are* in check (from a single piece, because the two checks + # case was handled above already). If the piece is a slider, we'll + # extract the ray from it to our king and add the checking piece to + # it, meaning the only legal moves are those that either block the + # check or capture the checking piece. For other non-sliding pieces + # the ray will be empty so the only legal move will be to capture + # the checking piece + let checker = self.position.checkers.lowestSquare() + mask = getRayBetween(checker, self.getBitboard(King, sideToMove).toSquare()) or checker.toBitboard() + + self.generatePawnMoves(moves, mask) + self.generateKnightMoves(moves, mask) + self.generateRookMoves(moves, mask) + self.generateBishopMoves(moves, mask) + # Queens are just handled rooks + bishops + + +proc isLegal(self: Chessboard, move: Move): bool {.inline.} = + ## Returns whether the given move is legal + var moves = MoveList() + self.generateMoves(moves) + return move in moves + + +proc makeMove*(self: Chessboard, move: Move): Move {.discardable.} = + ## Makes a move on the board + result = move + 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 diff --git a/Chess/nimfish/nimfishpkg/tui.nim b/Chess/nimfish/nimfishpkg/tui.nim index e373f81..4fb7dc8 100644 --- a/Chess/nimfish/nimfishpkg/tui.nim +++ b/Chess/nimfish/nimfishpkg/tui.nim @@ -1,4 +1,4 @@ -import ../nimfish +import movegen import std/strformat