From fe7e64e9c3be489df87891438b2a1bfa21eb81db Mon Sep 17 00:00:00 2001 From: nocturn9x Date: Wed, 1 May 2024 19:46:28 +0200 Subject: [PATCH] Refactor chessboard code and move most logic to Position --- Chess/nimfish/nimfishpkg/bitboards.nim | 33 +- Chess/nimfish/nimfishpkg/board.nim | 500 +------------------------ Chess/nimfish/nimfishpkg/eval.nim | 100 ++--- Chess/nimfish/nimfishpkg/movegen.nim | 152 ++++---- Chess/nimfish/nimfishpkg/moves.nim | 1 - Chess/nimfish/nimfishpkg/pieces.nim | 1 + Chess/nimfish/nimfishpkg/position.nim | 494 ++++++++++++++++++++++++ Chess/nimfish/nimfishpkg/search.nim | 15 +- Chess/nimfish/nimfishpkg/tui.nim | 31 +- Chess/nimfish/nimfishpkg/uci.nim | 8 +- 10 files changed, 674 insertions(+), 661 deletions(-) diff --git a/Chess/nimfish/nimfishpkg/bitboards.nim b/Chess/nimfish/nimfishpkg/bitboards.nim index 0be7202..e2a9800 100644 --- a/Chess/nimfish/nimfishpkg/bitboards.nim +++ b/Chess/nimfish/nimfishpkg/bitboards.nim @@ -262,9 +262,9 @@ func shortKnightDownRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard # We precompute as much stuff as possible: lookup tables are fast! -func computeKingBitboards: array[64, Bitboard] {.compileTime.} = +func computeKingBitboards: array[Square(0)..Square(63), Bitboard] {.compileTime.} = ## Precomputes all the movement bitboards for the king - for i in 0'u64..63: + for i in Square(0)..Square(63): let king = i.toBitboard() # It doesn't really matter which side we generate # the move for, they're identical for both @@ -284,9 +284,9 @@ func computeKingBitboards: array[64, Bitboard] {.compileTime.} = result[i] = movements -func computeKnightBitboards: array[64, Bitboard] {.compileTime.} = +func computeKnightBitboards: array[Square(0)..Square(63), Bitboard] {.compileTime.} = ## Precomputes all the movement bitboards for knights - for i in 0'u64..63: + for i in Square(0)..Square(63): let knight = i.toBitboard() # It doesn't really matter which side we generate # the move for, they're identical for both @@ -302,28 +302,19 @@ func computeKnightBitboards: array[64, Bitboard] {.compileTime.} = result[i] = movements -func computePawnAttacks(color: PieceColor): array[64, Bitboard] {.compileTime.} = +func computePawnAttacks(color: PieceColor): array[Square(0)..Square(63), Bitboard] {.compileTime.} = ## Precomputes all the attack bitboards for pawns ## of the given color - for i in 0'u64..63: - let - pawn = i.toBitboard() - square = Square(i) - file = fileFromSquare(square) - var movements = Bitboard(0) - if file in 1..7: - movements = movements or pawn.forwardLeftRelativeTo(color) - if file in 0..6: - movements = movements or pawn.forwardRightRelativeTo(color) - movements = movements and not pawn - result[i] = movements + for i in Square(0)..Square(63): + let pawn = i.toBitboard() + result[i] = pawn.backwardLeftRelativeTo(color) or pawn.backwardRightRelativeTo(color) const KING_BITBOARDS = computeKingBitboards() KNIGHT_BITBOARDS = computeKnightBitboards() - PAWN_ATTACKS = [computePawnAttacks(White), computePawnAttacks(Black)] + PAWN_ATTACKS: array[PieceColor.White..PieceColor.Black, array[Square(0)..Square(63), Bitboard]] = [computePawnAttacks(White), computePawnAttacks(Black)] -func getKingAttacks*(square: Square): Bitboard {.inline.} = KING_BITBOARDS[square.int] -func getKnightAttacks*(square: Square): Bitboard {.inline.} = KNIGHT_BITBOARDS[square.int] -func getPawnAttacks*(color: PieceColor, square: Square): Bitboard {.inline.} = PAWN_ATTACKS[color.int][square.int] +func getKingAttacks*(square: Square): Bitboard {.inline.} = KING_BITBOARDS[square] +func getKnightAttacks*(square: Square): Bitboard {.inline.} = KNIGHT_BITBOARDS[square] +func getPawnAttacks*(color: PieceColor, square: Square): Bitboard {.inline.} = PAWN_ATTACKS[color][square] diff --git a/Chess/nimfish/nimfishpkg/board.nim b/Chess/nimfish/nimfishpkg/board.nim index b281b82..f810de8 100644 --- a/Chess/nimfish/nimfishpkg/board.nim +++ b/Chess/nimfish/nimfishpkg/board.nim @@ -13,8 +13,6 @@ # limitations under the License. ## Implementation of a simple chessboard -import std/strformat -import std/strutils import pieces @@ -41,10 +39,7 @@ type positions*: seq[Position] -# A bunch of simple utility functions and forward declarations proc toFEN*(self: Chessboard): string -proc updateChecksAndPins*(self: var Chessboard) -proc hash*(self: var Chessboard) proc newChessboard*: Chessboard = @@ -54,118 +49,11 @@ proc newChessboard*: Chessboard = result.position.mailbox[i] = nullPiece() -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.position.mailbox[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).uint16 - of 5: - # Fullmove number - var s = "" - while index <= fen.high(): - s.add(fen[index]) - inc(index) - result.position.fullMoveCount = parseInt(s).uint16 - else: - raise newException(ValueError, "invalid FEN: too many fields in FEN string") - inc(index) - result.updateChecksAndPins() - result.hash() + result.position = loadFEN(fen) proc newDefaultChessboard*: Chessboard {.inline.} = @@ -174,375 +62,29 @@ proc newDefaultChessboard*: Chessboard {.inline.} = 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 getPiece*(self: Chessboard, square: Square): Piece {.inline.} = - ## Gets the piece at the given square - return self.position.mailbox[square] - - -func getPiece*(self: Chessboard, square: string): Piece {.inline.} = - ## Gets the piece on the given square - ## in algebraic notation - return self.getPiece(square.toSquare()) - - -proc removePieceFromBitboard*(self: var Chessboard, square: Square) = - ## Removes a piece at the given square in the chessboard from - ## its respective bitboard - let piece = self.getPiece(square) - self.position.pieces[piece.color][piece.kind].clearBit(square) - - -proc addPieceToBitboard*(self: var 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: var Chessboard, square: Square, piece: Piece) = - ## Internal helper to "spawn" a given piece at the given - ## square - when not defined(danger): - doAssert self.getPiece(square).kind == Empty - self.addPieceToBitboard(square, piece) - self.position.mailbox[square] = piece - - -proc removePiece*(self: var Chessboard, square: Square) = - ## Removes a piece from the board, updating necessary - ## metadata - when not defined(danger): - let piece = self.getPiece(square) - doAssert piece.kind != Empty and piece.color != None, self.toFEN() - self.removePieceFromBitboard(square) - self.position.mailbox[square] = nullPiece() - - -proc movePiece*(self: var Chessboard, move: Move) = - ## Internal helper to move a piece from - ## its current square to a target square - let piece = self.getPiece(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 movePiece*(self: var Chessboard, startSquare, targetSquare: Square) = - self.movePiece(createMove(startSquare, targetSquare)) - - -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 getOccupancyFor*(self: Chessboard, color: PieceColor): Bitboard {.inline.} = - ## Get the occupancy bitboard for every piece of the given color - result = self.position.getOccupancyFor(color) - - -func getOccupancy*(self: Chessboard): Bitboard {.inline.} = - ## Get the occupancy bitboard for every piece on - ## the chessboard - result = self.position.getOccupancy() - - -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) - squareBB = square.toBitboard() - if (getKingAttacks(square) and squareBB) != 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) - squareBB = square.toBitboard() - result = Bitboard(0) - for knight in knights: - if (getKnightAttacks(knight) and squareBB) != 0: - result = result or knight.toBitboard() - - -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: var 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() == 1: - 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() == 1: - 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 + return self.position.inCheck() -proc canCastle*(self: Chessboard): tuple[queen, king: bool] = +proc canCastle*(self: Chessboard): tuple[queen, king: bool] {.inline.} = ## Returns if the current side to move can castle - if self.inCheck(): - return (false, false) - let - sideToMove = self.position.sideToMove - occupancy = self.getOccupancy() - result = self.position.castlingAvailability[sideToMove] - if result.king: - result.king = (kingSideCastleRay(sideToMove) and occupancy) == 0 - if result.queen: - result.queen = (queenSideCastleRay(sideToMove) and occupancy) == 0 - if result.king: - # There are no pieces in between our friendly king and - # rook: check for attacks - let - king = self.getBitboard(King, sideToMove).toSquare() - for square in getRayBetween(king, sideToMove.kingSideRook()): - if self.isOccupancyAttacked(square, occupancy): - result.king = false - break - - if result.queen: - let - king: Square = self.getBitboard(King, sideToMove).toSquare() - # The king always moves two squares, but the queen side rook moves - # 3 squares. We only need to check for attacks on the squares where - # the king moves to and not any further. We subtract 3 instead of 2 - # because getRayBetween ignores the start and target squares in the - # ray it returns so we have to extend it by one - destination = makeSquare(rankFromSquare(king), fileFromSquare(king) - 3) - for square in getRayBetween(king, destination): - if self.isOccupancyAttacked(square, occupancy): - result.queen = false - break + return self.position.canCastle() -proc `$`*(self: Chessboard): string = - result &= "- - - - - - - -" - var file = 8 - for i in 0..7: - result &= "\n" - for j in 0..7: - let piece = self.position.mailbox[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 `$`*(self: Chessboard): string = $self.position 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.position.mailbox[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" + ## current position for easier visualization + return self.position.pretty() 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.position.mailbox[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 + return self.position.toFEN() proc drawByRepetition*(self: var Chessboard): bool = @@ -559,29 +101,3 @@ proc drawByRepetition*(self: var Chessboard): bool = return true dec(i) - -proc hash*(self: var Chessboard) = - ## Computes the zobrist hash of the current - ## position. This only needs to be called when - ## a position is loaded the first time, as all - ## subsequent hashes are updated incrementally - ## at every call to doMove() - self.position.zobristKey = ZobristKey(0) - - if self.position.sideToMove == Black: - self.position.zobristKey = self.position.zobristKey xor getBlackToMoveKey() - - for sq in self.getOccupancy(): - self.position.zobristKey = self.position.zobristKey xor self.getPiece(sq).getKey(sq) - - if self.position.castlingAvailability[White].king: - self.position.zobristKey = self.position.zobristKey xor getKingSideCastlingKey(White) - if self.position.castlingAvailability[White].queen: - self.position.zobristKey = self.position.zobristKey xor getQueenSideCastlingKey(White) - if self.position.castlingAvailability[Black].king: - self.position.zobristKey = self.position.zobristKey xor getKingSideCastlingKey(Black) - if self.position.castlingAvailability[Black].queen: - self.position.zobristKey = self.position.zobristKey xor getQueenSideCastlingKey(Black) - - if self.position.enPassantSquare != nullSquare(): - self.position.zobristKey = self.position.zobristKey xor getEnPassantKey(fileFromSquare(self.position.enPassantSquare)) diff --git a/Chess/nimfish/nimfishpkg/eval.nim b/Chess/nimfish/nimfishpkg/eval.nim index c38e94b..915ac1d 100644 --- a/Chess/nimfish/nimfishpkg/eval.nim +++ b/Chess/nimfish/nimfishpkg/eval.nim @@ -21,7 +21,7 @@ type # Stolen from https://www.chessprogramming.org/PeSTO's_Evaluation_Function const - PAWN_MIDDLEGAME_SCORES: array[64, Score] = [ + PAWN_MIDDLEGAME_SCORES: array[Square(0)..Square(63), Score] = [ 0, 0, 0, 0, 0, 0, 0, 0, 98, 134, 61, 95, 68, 126, 34, -11, -6, 7, 26, 31, 65, 56, 25, -20, @@ -32,7 +32,7 @@ const 0, 0, 0, 0, 0, 0, 0, 0, ] - PAWN_ENDGAME_SCORES: array[64, Score] = [ + PAWN_ENDGAME_SCORES: array[Square(0)..Square(63), Score] = [ 0, 0, 0, 0, 0, 0, 0, 0, 178, 173, 158, 134, 147, 132, 165, 187, 94, 100, 85, 67, 56, 53, 82, 84, @@ -43,7 +43,7 @@ const 0, 0, 0, 0, 0, 0, 0, 0, ] - KNIGHT_MIDDLEGAME_SCORES: array[64, Score] = [ + KNIGHT_MIDDLEGAME_SCORES: array[Square(0)..Square(63), Score] = [ -167, -89, -34, -49, 61, -97, -15, -107, -73, -41, 72, 36, 23, 62, 7, -17, -47, 60, 37, 65, 84, 129, 73, 44, @@ -54,7 +54,7 @@ const -105, -21, -58, -33, -17, -28, -19, -23, ] - KNIGHT_ENDGAME_SCORES: array[64, Score] = [ + KNIGHT_ENDGAME_SCORES: array[Square(0)..Square(63), Score] = [ -58, -38, -13, -28, -31, -27, -63, -99, -25, -8, -25, -2, -9, -25, -24, -52, -24, -20, 10, 9, -1, -9, -19, -41, @@ -65,7 +65,7 @@ const -29, -51, -23, -15, -22, -18, -50, -64, ] - BISHOP_MIDDLEGAME_SCORES: array[64, Score] = [ + BISHOP_MIDDLEGAME_SCORES: array[Square(0)..Square(63), Score] = [ -29, 4, -82, -37, -25, -42, 7, -8, -26, 16, -18, -13, 30, 59, 18, -47, -16, 37, 43, 40, 35, 50, 37, -2, @@ -76,7 +76,7 @@ const -33, -3, -14, -21, -13, -12, -39, -21, ] - BISHOP_ENDGAME_SCORES: array[64, Score] = [ + BISHOP_ENDGAME_SCORES: array[Square(0)..Square(63), Score] = [ -14, -21, -11, -8, -7, -9, -17, -24, -8, -4, 7, -12, -3, -13, -4, -14, 2, -8, 0, -1, -2, 6, 0, 4, @@ -87,7 +87,7 @@ const -23, -9, -23, -5, -9, -16, -5, -17, ] - ROOK_MIDDLEGAME_SCORES: array[64, Score] = [ + ROOK_MIDDLEGAME_SCORES: array[Square(0)..Square(63), Score] = [ 32, 42, 32, 51, 63, 9, 31, 43, 27, 32, 58, 62, 80, 67, 26, 44, -5, 19, 26, 36, 17, 45, 61, 16, @@ -98,7 +98,7 @@ const -19, -13, 1, 17, 16, 7, -37, -26, ] - ROOK_ENDGAME_SCORES: array[64, Score] = [ + ROOK_ENDGAME_SCORES: array[Square(0)..Square(63), Score] = [ 13, 10, 18, 15, 12, 12, 8, 5, 11, 13, 13, 11, -3, 3, 8, 3, 7, 7, 7, 5, 4, -3, -5, -3, @@ -109,7 +109,7 @@ const -9, 2, 3, -1, -5, -13, 4, -20, ] - QUEEN_MIDDLEGAME_SCORES: array[64, Score] = [ + QUEEN_MIDDLEGAME_SCORES: array[Square(0)..Square(63), Score] = [ -28, 0, 29, 12, 59, 44, 43, 45, -24, -39, -5, 1, -16, 57, 28, 54, -13, -17, 7, 8, 29, 56, 47, 57, @@ -120,7 +120,7 @@ const -1, -18, -9, 10, -15, -25, -31, -50, ] - QUEEN_ENDGAME_SCORES: array[64, Score] = [ + QUEEN_ENDGAME_SCORES: array[Square(0)..Square(63), Score] = [ -9, 22, 22, 27, 27, 19, 10, 20, -17, 20, 32, 41, 58, 25, 30, 0, -20, 6, 9, 49, 47, 35, 19, 9, @@ -131,7 +131,7 @@ const -33, -28, -22, -43, -5, -32, -20, -41, ] - KING_MIDDLEGAME_SCORES: array[64, Score] = [ + KING_MIDDLEGAME_SCORES: array[Square(0)..Square(63), Score] = [ -65, 23, 16, -15, -56, -34, 2, 13, 29, -1, -20, -7, -8, -4, -38, -29, -9, 24, 2, -16, -20, 6, 22, -22, @@ -142,7 +142,7 @@ const -15, 36, 12, -54, 8, -28, 24, 14, ] - KING_ENDGAME_SCORES: array[64, Score] = [ + KING_ENDGAME_SCORES: array[Square(0)..Square(63), Score] = [ -74, -35, -18, -18, -11, 15, 4, -17, -12, 17, 14, 17, 17, 38, 23, 11, 10, 17, 23, 15, 20, 45, 44, 13, @@ -154,10 +154,10 @@ const ] # Bishop, King, Knight, Pawn, Queen, Rook - MIDDLEGAME_WEIGHTS: array[6, Score] = [365, 0, 337, 82, 1025, 477] - ENDGAME_WEIGHTS: array[6, Score] = [297, 0, 281, 94, 936, 512] + MIDDLEGAME_WEIGHTS: array[PieceKind.Bishop..PieceKind.Rook, Score] = [365, 0, 337, 82, 1025, 477] + ENDGAME_WEIGHTS: array[PieceKind.Bishop..PieceKind.Rook, Score] = [297, 0, 281, 94, 936, 512] - MIDDLEGAME_PSQ_TABLES: array[6, array[64, Score]] = [ + MIDDLEGAME_PSQ_TABLES: array[PieceKind.Bishop..PieceKind.Rook, array[Square(0)..Square(63), Score]] = [ BISHOP_MIDDLEGAME_SCORES, KING_MIDDLEGAME_SCORES, KNIGHT_MIDDLEGAME_SCORES, @@ -166,7 +166,7 @@ const ROOK_MIDDLEGAME_SCORES ] - ENDGAME_PSQ_TABLES: array[6, array[64, Score]] = [ + ENDGAME_PSQ_TABLES: array[PieceKind.Bishop..PieceKind.Rook, array[Square(0)..Square(63), Score]] = [ BISHOP_ENDGAME_SCORES, KING_ENDGAME_SCORES, KNIGHT_ENDGAME_SCORES, @@ -180,18 +180,18 @@ const var - MIDDLEGAME_VALUE_TABLES: array[2, array[6, array[64, Score]]] - ENDGAME_VALUE_TABLES: array[2, array[6, array[64, Score]]] + MIDDLEGAME_VALUE_TABLES: array[PieceColor.White..PieceColor.Black, array[PieceKind.Bishop..PieceKind.Rook, array[Square(0)..Square(63), Score]]] + ENDGAME_VALUE_TABLES: array[PieceColor.White..PieceColor.Black, array[PieceKind.Bishop..PieceKind.Rook, array[Square(0)..Square(63), Score]]] proc initializeTables = for kind in [Bishop, King, Knight, Pawn, Queen, Rook]: - for sq in 0..63: - MIDDLEGAME_VALUE_TABLES[White.int][kind.int][sq] = MIDDLEGAME_WEIGHTS[kind.int] + MIDDLEGAME_PSQ_TABLES[kind.int][sq] - ENDGAME_VALUE_TABLES[White.int][kind.int][sq] = ENDGAME_WEIGHTS[kind.int] + ENDGAME_PSQ_TABLES[kind.int][sq] - MIDDLEGAME_VALUE_TABLES[Black.int][kind.int][sq] = MIDDLEGAME_WEIGHTS[kind.int] + MIDDLEGAME_PSQ_TABLES[kind.int][sq xor 56] - ENDGAME_VALUE_TABLES[Black.int][kind.int][sq] = ENDGAME_WEIGHTS[kind.int] + ENDGAME_PSQ_TABLES[kind.int][sq xor 56] + for sq in Square(0)..Square(63): + MIDDLEGAME_VALUE_TABLES[White][kind][sq] = MIDDLEGAME_WEIGHTS[kind] + MIDDLEGAME_PSQ_TABLES[kind][sq] + ENDGAME_VALUE_TABLES[White][kind][sq] = ENDGAME_WEIGHTS[kind] + ENDGAME_PSQ_TABLES[kind][sq] + MIDDLEGAME_VALUE_TABLES[Black][kind][sq] = MIDDLEGAME_WEIGHTS[kind] + MIDDLEGAME_PSQ_TABLES[kind][sq xor 56] + ENDGAME_VALUE_TABLES[Black][kind][sq] = ENDGAME_WEIGHTS[kind] + ENDGAME_PSQ_TABLES[kind][sq xor 56] initializeTables() @@ -202,12 +202,12 @@ func highestEval*: Score {.inline.} = Score(25_000) func mateScore*: Score {.inline.} = lowestEval() + 1 -func getGamePhase(board: Chessboard): int {.inline.} = +func getGamePhase(position: Position): int {.inline.} = ## Computes the game phase according to ## how many pieces are left on the board result = 0 - for sq in board.getOccupancy(): - case board.getPiece(sq).kind: + for sq in position.getOccupancy(): + case position.getPiece(sq).kind: of Bishop, Knight: inc(result) of Queen: @@ -221,26 +221,26 @@ func getGamePhase(board: Chessboard): int {.inline.} = result = min(24, result) -proc getPieceScore*(board: Chessboard, square: Square): Score = +proc getPieceScore*(position: Position, square: Square): Score = ## Returns the value of the piece located at ## the given square let - piece = board.getPiece(square) - middleGameScore = MIDDLEGAME_VALUE_TABLES[piece.color.int][piece.kind.int][square.int] - endGameScore = ENDGAME_VALUE_TABLES[piece.color.int][piece.kind.int][square.int] - middleGamePhase = board.getGamePhase() + piece = position.getPiece(square) + middleGameScore = MIDDLEGAME_VALUE_TABLES[piece.color][piece.kind][square] + endGameScore = ENDGAME_VALUE_TABLES[piece.color][piece.kind][square] + middleGamePhase = position.getGamePhase() endGamePhase = 24 - middleGamePhase result = Score((middleGameScore * middleGamePhase + endGameScore * endGamePhase) div 24) -proc getPieceScore*(board: Chessboard, piece: Piece, square: Square): Score = +proc getPieceScore*(position: Position, piece: Piece, square: Square): Score = ## Returns the value the given piece would have if it ## were at the given square let - middleGameScore = MIDDLEGAME_VALUE_TABLES[piece.color.int][piece.kind.int][square.int] - endGameScore = ENDGAME_VALUE_TABLES[piece.color.int][piece.kind.int][square.int] - middleGamePhase = board.getGamePhase() + middleGameScore = MIDDLEGAME_VALUE_TABLES[piece.color][piece.kind][square] + endGameScore = ENDGAME_VALUE_TABLES[piece.color][piece.kind][square] + middleGamePhase = position.getGamePhase() endGamePhase = 24 - middleGamePhase result = Score((middleGameScore * middleGamePhase + endGameScore * endGamePhase) div 24) @@ -250,33 +250,33 @@ proc evaluateMaterial(board: ChessBoard): Score = ## Returns a material and position evaluation ## for the current side to move let - middleGamePhase = board.getGamePhase() + middleGamePhase = board.position.getGamePhase() endGamePhase = 24 - middleGamePhase var # White, Black - middleGameScores: array[2, Score] = [0, 0] - endGameScores: array[2, Score] = [0, 0] + middleGameScores: array[PieceColor.White..PieceColor.Black, Score] = [0, 0] + endGameScores: array[PieceColor.White..PieceColor.Black, Score] = [0, 0] - for sq in board.getOccupancy(): - let piece = board.getPiece(sq) - middleGameScores[piece.color.int] += MIDDLEGAME_VALUE_TABLES[piece.color.int][piece.kind.int][sq.int] - endGameScores[piece.color.int] += ENDGAME_VALUE_TABLES[piece.color.int][piece.kind.int][sq.int] + for sq in board.position.getOccupancy(): + let piece = board.position.getPiece(sq) + middleGameScores[piece.color] += MIDDLEGAME_VALUE_TABLES[piece.color][piece.kind][sq] + endGameScores[piece.color] += ENDGAME_VALUE_TABLES[piece.color][piece.kind][sq] let sideToMove = board.position.sideToMove nonSideToMove = sideToMove.opposite() - middleGameScore = middleGameScores[sideToMove.int] - middleGameScores[nonSideToMove.int] - endGameScore = endGameScores[sideToMove.int] - endGameScores[nonSideToMove.int] + middleGameScore = middleGameScores[sideToMove] - middleGameScores[nonSideToMove] + endGameScore = endGameScores[sideToMove] - endGameScores[nonSideToMove] result = Score((middleGameScore * middleGamePhase + endGameScore * endGamePhase) div 24) -proc evaluatePawnStructure(board: Chessboard): Score {.used.} = +proc evaluatePawnStructure(position: Position): Score {.used.} = ## Evaluates the pawn structure of the current ## position for the side to move let - sideToMove = board.position.sideToMove - friendlyPawns = board.getOccupancyFor(sideToMove) + sideToMove = position.sideToMove + friendlyPawns = position.getOccupancyFor(sideToMove) # Doubled pawns are a bad idea var doubledPawns = 0 @@ -299,9 +299,9 @@ proc evaluatePawnStructure(board: Chessboard): Score {.used.} = # stronger var strongPawnIncrement = Score(0) - for pawn in board.getBitboard(Pawn, White): - if board.getPawnAttacks(pawn, White) != 0: - strongPawnIncrement += board.getPieceScore(pawn) div Score(4) + for pawn in position.getBitboard(Pawn, White): + if position.getPawnAttacks(pawn, White) != 0: + strongPawnIncrement += position.getPieceScore(pawn) div Score(4) return DOUBLED_PAWNS_MALUS[doubledPawns] + ISOLATED_PAWN_MALUS[isolatedPawns] + strongPawnIncrement diff --git a/Chess/nimfish/nimfishpkg/movegen.nim b/Chess/nimfish/nimfishpkg/movegen.nim index d801640..a7fa586 100644 --- a/Chess/nimfish/nimfishpkg/movegen.nim +++ b/Chess/nimfish/nimfishpkg/movegen.nim @@ -36,10 +36,10 @@ proc generatePawnMoves(self: var Chessboard, moves: var MoveList, destinationMas let sideToMove = self.position.sideToMove nonSideToMove = sideToMove.opposite() - pawns = self.getBitboard(Pawn, sideToMove) - occupancy = self.getOccupancy() + pawns = self.position.getBitboard(Pawn, sideToMove) + occupancy = self.position.getOccupancy() # We can only capture enemy pieces (except the king) - enemyPieces = self.getOccupancyFor(nonSideToMove) + enemyPieces = self.position.getOccupancyFor(nonSideToMove) epTarget = self.position.enPassantSquare diagonalPins = self.position.diagonalPins orthogonalPins = self.position.orthogonalPins @@ -48,7 +48,7 @@ proc generatePawnMoves(self: var Chessboard, moves: var MoveList, destinationMas # 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) - friendlyKing = self.getBitboard(King, sideToMove).toSquare() + friendlyKing = self.position.getBitboard(King, sideToMove).toSquare() # Single and double pushes @@ -146,33 +146,33 @@ proc generatePawnMoves(self: var Chessboard, moves: var MoveList, destinationMas # king is in check after en passant when it actually isn't # (see pos fen rnbqkbnr/pppp1ppp/8/2P5/K7/8/PPPP1PPP/RNBQ1BNR b kq - 0 1 moves b7b5 c5b6) let epPawnSquare = epPawn.toSquare() - let epPiece = self.getPiece(epPawnSquare) - self.removePiece(epPawnSquare) - if not self.isOccupancyAttacked(friendlyKing, newOccupancy): + let epPiece = self.position.getPiece(epPawnSquare) + self.position.removePiece(epPawnSquare) + if not self.position.isOccupancyAttacked(friendlyKing, newOccupancy): # En passant does not create a check on the king: all good moves.add(createMove(friendlyPawn, epBitboard, EnPassant)) - self.spawnPiece(epPawnSquare, epPiece) + self.position.spawnPiece(epPawnSquare, epPiece) if epRight != 0: # Note that this isn't going to be the same pawn from the previous if block! let friendlyPawn = epBitboard.backwardLeftRelativeTo(sideToMove) newOccupancy = occupancy and not epPawn and not friendlyPawn or epBitboard let epPawnSquare = epPawn.toSquare() - let epPiece = self.getPiece(epPawnSquare) - self.removePiece(epPawnSquare) - if not self.isOccupancyAttacked(friendlyKing, newOccupancy): + let epPiece = self.position.getPiece(epPawnSquare) + self.position.removePiece(epPawnSquare) + if not self.position.isOccupancyAttacked(friendlyKing, newOccupancy): # En passant does not create a check on the king: all good moves.add(createMove(friendlyPawn, epBitboard, EnPassant)) - self.spawnPiece(epPawnSquare, epPiece) + self.position.spawnPiece(epPawnSquare, epPiece) proc generateRookMoves(self: Chessboard, moves: var MoveList, destinationMask: Bitboard) = let sideToMove = self.position.sideToMove - occupancy = self.getOccupancy() - enemyPieces = self.getOccupancyFor(sideToMove.opposite()) and not self.getBitboard(King, sideToMove.opposite()) - rooks = self.getBitboard(Rook, sideToMove) - queens = self.getBitboard(Queen, sideToMove) + occupancy = self.position.getOccupancy() + enemyPieces = self.position.getOccupancyFor(sideToMove.opposite()) and not self.position.getBitboard(King, sideToMove.opposite()) + rooks = self.position.getBitboard(Rook, sideToMove) + queens = self.position.getBitboard(Queen, sideToMove) movableRooks = not self.position.diagonalPins and (queens or rooks) pinMask = self.position.orthogonalPins pinnedRooks = movableRooks and pinMask @@ -200,10 +200,10 @@ proc generateRookMoves(self: Chessboard, moves: var MoveList, destinationMask: B proc generateBishopMoves(self: Chessboard, moves: var MoveList, destinationMask: Bitboard) = let sideToMove = self.position.sideToMove - occupancy = self.getOccupancy() - enemyPieces = self.getOccupancyFor(sideToMove.opposite()) and not self.getBitboard(King, sideToMove.opposite()) - bishops = self.getBitboard(Bishop, sideToMove) - queens = self.getBitboard(Queen, sideToMove) + occupancy = self.position.getOccupancy() + enemyPieces = self.position.getOccupancyFor(sideToMove.opposite()) and not self.position.getBitboard(King, sideToMove.opposite()) + bishops = self.position.getBitboard(Bishop, sideToMove) + queens = self.position.getBitboard(Queen, sideToMove) movableBishops = not self.position.orthogonalPins and (queens or bishops) pinMask = self.position.diagonalPins pinnedBishops = movableBishops and pinMask @@ -229,29 +229,29 @@ proc generateBishopMoves(self: Chessboard, moves: var MoveList, destinationMask: proc generateKingMoves(self: Chessboard, moves: var MoveList, capturesOnly=false) = let sideToMove = self.position.sideToMove - king = self.getBitboard(King, sideToMove) - occupancy = self.getOccupancy() + king = self.position.getBitboard(King, sideToMove) + occupancy = self.position.getOccupancy() nonSideToMove = sideToMove.opposite() - enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove) + enemyPieces = self.position.getOccupancyFor(nonSideToMove) and not self.position.getBitboard(King, nonSideToMove) bitboard = getKingAttacks(king.toSquare()) noKingOccupancy = occupancy and not king if not capturesOnly: for square in bitboard and not occupancy: - if not self.isOccupancyAttacked(square, noKingOccupancy): + if not self.position.isOccupancyAttacked(square, noKingOccupancy): moves.add(createMove(king, square)) for square in bitboard and enemyPieces: - if not self.isOccupancyAttacked(square, noKingOccupancy): + if not self.position.isOccupancyAttacked(square, noKingOccupancy): moves.add(createMove(king, square, Capture)) proc generateKnightMoves(self: Chessboard, moves: var MoveList, destinationMask: Bitboard) = let sideToMove = self.position.sideToMove - knights = self.getBitboard(Knight, sideToMove) + knights = self.position.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) + enemyPieces = self.position.getOccupancyFor(nonSideToMove) and not self.position.getBitboard(King, nonSideToMove) for square in unpinnedKnights: let bitboard = getKnightAttacks(square) for target in bitboard and destinationMask and not enemyPieces: @@ -264,8 +264,8 @@ proc generateCastling(self: Chessboard, moves: var MoveList) = let sideToMove = self.position.sideToMove castlingRights = self.canCastle() - kingSquare = self.getBitboard(King, sideToMove).toSquare() - kingPiece = self.getPiece(kingSquare) + kingSquare = self.position.getBitboard(King, sideToMove).toSquare() + kingPiece = self.position.getPiece(kingSquare) if castlingRights.king: moves.add(createMove(kingSquare, kingPiece.kingSideCastling(), Castle)) if castlingRights.queen: @@ -296,7 +296,7 @@ proc generateMoves*(self: var Chessboard, moves: var MoveList, capturesOnly: boo var destinationMask: Bitboard if not self.inCheck(): # Not in check: cannot move over friendly pieces - destinationMask = not self.getOccupancyFor(sideToMove) + destinationMask = not self.position.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 @@ -309,8 +309,8 @@ proc generateMoves*(self: var Chessboard, moves: var MoveList, capturesOnly: boo checker = self.position.checkers.lowestSquare() checkerBB = checker.toBitboard() # epTarget = self.position.enPassantSquare - # checkerPiece = self.getPiece(checker) - destinationMask = getRayBetween(checker, self.getBitboard(King, sideToMove).toSquare()) or checkerBB + # checkerPiece = self.position.getPiece(checker) + destinationMask = getRayBetween(checker, self.position.getBitboard(King, sideToMove).toSquare()) or checkerBB # TODO: This doesn't really work. I've addressed the issue for now, but it's kinda ugly. Find a better # solution # if checkerPiece.kind == Pawn and checkerBB.backwardRelativeTo(checkerPiece.color).toSquare() == epTarget: @@ -322,7 +322,7 @@ proc generateMoves*(self: var Chessboard, moves: var MoveList, capturesOnly: boo if capturesOnly: # Note: This does not cover en passant (which is good because it's a capture, # but the "fix" stands on flimsy ground) - destinationMask = destinationMask and self.getOccupancyFor(nonSideToMove) + destinationMask = destinationMask and self.position.getOccupancyFor(nonSideToMove) self.generatePawnMoves(moves, destinationMask) self.generateKnightMoves(moves, destinationMask) self.generateRookMoves(moves, destinationMask) @@ -340,7 +340,7 @@ proc doMove*(self: var Chessboard, move: Move) = self.positions.add(self.position) # Final checks - let piece = self.getPiece(move.startSquare) + let piece = self.position.getPiece(move.startSquare) when not defined(danger): doAssert piece.kind != Empty and piece.color != None, &"{move} {self.toFEN()}" @@ -383,8 +383,8 @@ proc doMove*(self: var Chessboard, move: Move) = if move.isEnPassant(): # Make the en passant pawn disappear let epPawnSquare = move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare() - self.position.zobristKey = self.position.zobristKey xor self.getPiece(epPawnSquare).getKey(epPawnSquare) - self.removePiece(epPawnSquare) + self.position.zobristKey = self.position.zobristKey xor self.position.getPiece(epPawnSquare).getKey(epPawnSquare) + self.position.removePiece(epPawnSquare) if move.isCastling() or piece.kind == King: # If the king has moved, all castling rights for the side to @@ -399,15 +399,15 @@ proc doMove*(self: var Chessboard, move: Move) = if move.targetSquare == piece.kingSideCastling(): source = piece.color.kingSideRook() - rook = self.getPiece(source) + rook = self.position.getPiece(source) target = rook.kingSideCastling() elif move.targetSquare == piece.queenSideCastling(): source = piece.color.queenSideRook() - rook = self.getPiece(source) + rook = self.position.getPiece(source) target = rook.queenSideCastling() - self.movePiece(source, target) + self.position.movePiece(source, target) self.position.zobristKey = self.position.zobristKey xor piece.getKey(source) self.position.zobristKey = self.position.zobristKey xor piece.getKey(target) @@ -421,9 +421,9 @@ proc doMove*(self: var Chessboard, move: Move) = if move.isCapture(): # Get rid of captured pieces - let captured = self.getPiece(move.targetSquare) + let captured = self.position.getPiece(move.targetSquare) self.position.zobristKey = self.position.zobristKey xor captured.getKey(move.targetSquare) - self.removePiece(move.targetSquare) + self.position.removePiece(move.targetSquare) # If a rook has been captured, castling on that side is prohibited if captured.kind == Rook: if move.targetSquare == captured.color.kingSideRook(): @@ -432,13 +432,13 @@ proc doMove*(self: var Chessboard, move: Move) = self.position.castlingAvailability[captured.color].queen = false # Move the piece to its target square - self.movePiece(move) + self.position.movePiece(move) self.position.zobristKey = self.position.zobristKey xor piece.getKey(move.startSquare) self.position.zobristKey = self.position.zobristKey xor piece.getKey(move.targetSquare) if move.isPromotion(): # Move is a pawn promotion: get rid of the pawn # and spawn a new piece - self.removePiece(move.targetSquare) + self.position.removePiece(move.targetSquare) self.position.zobristKey = self.position.zobristKey xor piece.getKey(move.targetSquare) var spawnedPiece: Piece case move.getPromotionType(): @@ -454,9 +454,9 @@ proc doMove*(self: var Chessboard, move: Move) = # Unreachable discard self.position.zobristKey = self.position.zobristKey xor spawnedPiece.getKey(move.targetSquare) - self.spawnPiece(move.targetSquare, spawnedPiece) + self.position.spawnPiece(move.targetSquare, spawnedPiece) # Updates checks and pins for the (new) side to move - self.updateChecksAndPins() + self.position.updateChecksAndPins() # Last updates to zobrist key if self.position.castlingAvailability[piece.color].king: self.position.zobristKey = self.position.zobristKey xor getKingSideCastlingKey(piece.color) @@ -496,7 +496,7 @@ proc testPiece(piece: Piece, kind: PieceKind, color: PieceColor) = proc testPieceCount(board: Chessboard, kind: PieceKind, color: PieceColor, count: int) = - let pieces = board.countPieces(kind, color) + let pieces = board.position.countPieces(kind, color) doAssert pieces == count, &"expected {count} pieces of kind {kind} and color {color}, got {pieces} instead" @@ -553,45 +553,45 @@ proc basicTests* = # Pawns for loc in ["a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2"]: - testPiece(board.getPiece(loc), Pawn, White) + testPiece(board.position.getPiece(loc), Pawn, White) for loc in ["a7", "b7", "c7", "d7", "e7", "f7", "g7", "h7"]: - testPiece(board.getPiece(loc), Pawn, Black) + testPiece(board.position.getPiece(loc), Pawn, Black) # Rooks - testPiece(board.getPiece("a1"), Rook, White) - testPiece(board.getPiece("h1"), Rook, White) - testPiece(board.getPiece("a8"), Rook, Black) - testPiece(board.getPiece("h8"), Rook, Black) + testPiece(board.position.getPiece("a1"), Rook, White) + testPiece(board.position.getPiece("h1"), Rook, White) + testPiece(board.position.getPiece("a8"), Rook, Black) + testPiece(board.position.getPiece("h8"), Rook, Black) # Knights - testPiece(board.getPiece("b1"), Knight, White) - testPiece(board.getPiece("g1"), Knight, White) - testPiece(board.getPiece("b8"), Knight, Black) - testPiece(board.getPiece("g8"), Knight, Black) + testPiece(board.position.getPiece("b1"), Knight, White) + testPiece(board.position.getPiece("g1"), Knight, White) + testPiece(board.position.getPiece("b8"), Knight, Black) + testPiece(board.position.getPiece("g8"), Knight, Black) # Bishops - testPiece(board.getPiece("c1"), Bishop, White) - testPiece(board.getPiece("f1"), Bishop, White) - testPiece(board.getPiece("c8"), Bishop, Black) - testPiece(board.getPiece("f8"), Bishop, Black) + testPiece(board.position.getPiece("c1"), Bishop, White) + testPiece(board.position.getPiece("f1"), Bishop, White) + testPiece(board.position.getPiece("c8"), Bishop, Black) + testPiece(board.position.getPiece("f8"), Bishop, Black) # Kings - testPiece(board.getPiece("e1"), King, White) - testPiece(board.getPiece("e8"), King, Black) + testPiece(board.position.getPiece("e1"), King, White) + testPiece(board.position.getPiece("e8"), King, Black) # Queens - testPiece(board.getPiece("d1"), Queen, White) - testPiece(board.getPiece("d8"), Queen, Black) + testPiece(board.position.getPiece("d1"), Queen, White) + testPiece(board.position.getPiece("d8"), Queen, Black) # Ensure our bitboards match with the board let - whitePawns = board.getBitboard(Pawn, White) - whiteKnights = board.getBitboard(Knight, White) - whiteBishops = board.getBitboard(Bishop, White) - whiteRooks = board.getBitboard(Rook, White) - whiteQueens = board.getBitboard(Queen, White) - whiteKing = board.getBitboard(King, White) - blackPawns = board.getBitboard(Pawn, Black) - blackKnights = board.getBitboard(Knight, Black) - blackBishops = board.getBitboard(Bishop, Black) - blackRooks = board.getBitboard(Rook, Black) - blackQueens = board.getBitboard(Queen, Black) - blackKing = board.getBitboard(King, Black) + whitePawns = board.position.getBitboard(Pawn, White) + whiteKnights = board.position.getBitboard(Knight, White) + whiteBishops = board.position.getBitboard(Bishop, White) + whiteRooks = board.position.getBitboard(Rook, White) + whiteQueens = board.position.getBitboard(Queen, White) + whiteKing = board.position.getBitboard(King, White) + blackPawns = board.position.getBitboard(Pawn, Black) + blackKnights = board.position.getBitboard(Knight, Black) + blackBishops = board.position.getBitboard(Bishop, Black) + blackRooks = board.position.getBitboard(Rook, Black) + blackQueens = board.position.getBitboard(Queen, Black) + blackKing = board.position.getBitboard(King, Black) whitePawnSquares = @[makeSquare(6'i8, 0'i8), makeSquare(6, 1), makeSquare(6, 2), makeSquare(6, 3), makeSquare(6, 4), makeSquare(6, 5), makeSquare(6, 6), makeSquare(6, 7)] whiteKnightSquares = @[makeSquare(7'i8, 1'i8), makeSquare(7, 6)] whiteBishopSquares = @[makeSquare(7'i8, 2'i8), makeSquare(7, 5)] diff --git a/Chess/nimfish/nimfishpkg/moves.nim b/Chess/nimfish/nimfishpkg/moves.nim index 1473d11..60fdbe6 100644 --- a/Chess/nimfish/nimfishpkg/moves.nim +++ b/Chess/nimfish/nimfishpkg/moves.nim @@ -183,7 +183,6 @@ func `$`*(self: Move): string = result &= ")" - func toAlgebraic*(self: Move): string = if self == nullMove(): return "null" diff --git a/Chess/nimfish/nimfishpkg/pieces.nim b/Chess/nimfish/nimfishpkg/pieces.nim index 01f31b7..4067620 100644 --- a/Chess/nimfish/nimfishpkg/pieces.nim +++ b/Chess/nimfish/nimfishpkg/pieces.nim @@ -51,6 +51,7 @@ func isValid*(a: Square): bool {.inline.} = a.int8 in 0..63 func isLightSquare*(a: Square): bool {.inline.} = (a.int8 and 2) == 0 # Overridden operators for our distinct type +func `xor`*(a: Square, b: SomeInteger): Square {.inline.} = Square(a.int8 xor b) func `==`*(a, b: Square): bool {.inline.} = a.int8 == b.int8 func `!=`*(a, b: Square): bool {.inline.} = a.int8 != b.int8 func `<`*(a: Square, b: SomeInteger): bool {.inline.} = a.int8 < b.int8 diff --git a/Chess/nimfish/nimfishpkg/position.nim b/Chess/nimfish/nimfishpkg/position.nim index 432dc54..ef22aac 100644 --- a/Chess/nimfish/nimfishpkg/position.nim +++ b/Chess/nimfish/nimfishpkg/position.nim @@ -11,10 +11,18 @@ # 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/strformat +import std/strutils + + import bitboards import magics import pieces import zobrist +import moves +import rays + +export bitboards, magics, pieces, zobrist, moves, rays type @@ -57,6 +65,11 @@ type mailbox*: array[Square(0)..Square(63), Piece] +func inCheck*(self: Position): bool {.inline.} = + ## Returns if the current side to move is in check + return self.checkers != 0 + + func getKingStartingSquare*(color: PieceColor): Square {.inline.} = ## Retrieves the starting square of the king ## for the given color @@ -90,3 +103,484 @@ func getOccupancy*(self: Position): Bitboard {.inline.} = ## Get the occupancy bitboard for every piece on ## the chessboard result = self.getOccupancyFor(Black) or self.getOccupancyFor(White) + + +func getPawnAttacks*(self: Position, square: Square, attacker: PieceColor): Bitboard {.inline.} = + ## Returns the locations of the pawns attacking the given square + return self.getBitboard(Pawn, attacker) and getPawnAttacks(attacker, square) + + +func getKingAttacks*(self: Position, 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) + squareBB = square.toBitboard() + if (getKingAttacks(square) and squareBB) != 0: + result = result or king + + +func getKnightAttacks*(self: Position, square: Square, attacker: PieceColor): Bitboard = + ## Returns the locations of the knights attacking the given square + let + knights = self.getBitboard(Knight, attacker) + squareBB = square.toBitboard() + result = Bitboard(0) + for knight in knights: + if (getKnightAttacks(knight) and squareBB) != 0: + result = result or knight.toBitboard() + + +proc getSlidingAttacks*(self: Position, 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: Position, square: Square, attacker: PieceColor): Bitboard = + ## Computes the attack bitboard for the given square from + ## the given side + result = Bitboard(0) 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: Position, 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, for example, 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 + nonSideToMove = self.sideToMove.opposite() + knights = self.getBitboard(Knight, nonSideToMove) + + 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 + + if self.getPawnAttacks(square, nonSideToMove) != 0: + return true + + +proc canCastle*(self: Position): tuple[queen, king: bool] = + ## Returns if the current side to move can castle + if self.inCheck(): + return (false, false) + let + sideToMove = self.sideToMove + occupancy = self.getOccupancy() + result = self.castlingAvailability[sideToMove] + if result.king: + result.king = (kingSideCastleRay(sideToMove) and occupancy) == 0 + if result.queen: + result.queen = (queenSideCastleRay(sideToMove) and occupancy) == 0 + if result.king: + # There are no pieces in between our friendly king and + # rook: check for attacks + let + king = self.getBitboard(King, sideToMove).toSquare() + for square in getRayBetween(king, sideToMove.kingSideRook()): + if self.isOccupancyAttacked(square, occupancy): + result.king = false + break + + if result.queen: + let + king: Square = self.getBitboard(King, sideToMove).toSquare() + # The king always moves two squares, but the queen side rook moves + # 3 squares. We only need to check for attacks on the squares where + # the king moves to and not any further. We subtract 3 instead of 2 + # because getRayBetween ignores the start and target squares in the + # ray it returns so we have to extend it by one + destination = makeSquare(rankFromSquare(king), fileFromSquare(king) - 3) + for square in getRayBetween(king, destination): + if self.isOccupancyAttacked(square, occupancy): + result.queen = false + break + + +func countPieces*(self: Position, kind: PieceKind, color: PieceColor): int {.inline.} = + ## Returns the number of pieces with + ## the given color and type in the + ## position + return self.pieces[color][kind].countSquares() + + +func getPiece*(self: Position, square: Square): Piece {.inline.} = + ## Gets the piece at the given square in + ## the position + return self.mailbox[square] + + +func getPiece*(self: Position, square: string): Piece {.inline.} = + ## Gets the piece on the given square + ## in algebraic notation + return self.getPiece(square.toSquare()) + + +proc removePieceFromBitboard*(self: var Position, square: Square) = + ## Removes a piece at the given square from + ## its respective bitboard + let piece = self.getPiece(square) + self.pieces[piece.color][piece.kind].clearBit(square) + + +proc addPieceToBitboard*(self: var Position, square: Square, piece: Piece) = + ## Adds the given piece at the given square to + ## its respective bitboard + self.pieces[piece.color][piece.kind].setBit(square) + + +proc spawnPiece*(self: var Position, square: Square, piece: Piece) = + ## Spawns a new piece at the given square + when not defined(danger): + doAssert self.getPiece(square).kind == Empty + self.addPieceToBitboard(square, piece) + self.mailbox[square] = piece + + +proc removePiece*(self: var Position, square: Square) = + ## Removes a piece from the board, updating necessary + ## metadata + when not defined(danger): + let piece = self.getPiece(square) + doAssert piece.kind != Empty and piece.color != None, self.toFEN() + self.removePieceFromBitboard(square) + self.mailbox[square] = nullPiece() + + +proc movePiece*(self: var Position, move: Move) = + ## Internal helper to move a piece from + ## its current square to a target square + let piece = self.getPiece(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 movePiece*(self: var Position, startSquare, targetSquare: Square) = + ## Moves a piece from the given start square to the given + ## target square + self.movePiece(createMove(startSquare, targetSquare)) + + +func countPieces*(self: Position, piece: Piece): int {.inline.} = + ## Returns the number of pieces in the position that + ## are of the same type and color as the given piece + return self.countPieces(piece.kind, piece.color) + + +proc updateChecksAndPins*(self: var Position) = + ## 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.sideToMove + nonSideToMove = sideToMove.opposite() + friendlyKing = self.getBitboard(King, sideToMove).toSquare() + friendlyPieces = self.getOccupancyFor(sideToMove) + enemyPieces = self.getOccupancyFor(nonSideToMove) + + # Update checks + self.checkers = self.getAttacksTo(friendlyKing, nonSideToMove) + # Update pins + self.diagonalPins = Bitboard(0) + self.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() == 1: + self.diagonalPins = self.diagonalPins or pinningRay + + for piece in canPinOrthogonally: + let pinningRay = getRayBetween(friendlyKing, piece) or piece.toBitboard() + if (pinningRay and friendlyPieces).countSquares() == 1: + self.orthogonalPins = self.orthogonalPins or pinningRay + + +proc hash*(self: var Position) = + ## Computes the zobrist hash of the position + ## This only needs to be called when a position + ## is loaded the first time, as all subsequent + ## hashes are updated incrementally at every + ## call to doMove() + self.zobristKey = ZobristKey(0) + + if self.sideToMove == Black: + self.zobristKey = self.zobristKey xor getBlackToMoveKey() + + for sq in self.getOccupancy(): + self.zobristKey = self.zobristKey xor self.getPiece(sq).getKey(sq) + + if self.castlingAvailability[White].king: + self.zobristKey = self.zobristKey xor getKingSideCastlingKey(White) + if self.castlingAvailability[White].queen: + self.zobristKey = self.zobristKey xor getQueenSideCastlingKey(White) + if self.castlingAvailability[Black].king: + self.zobristKey = self.zobristKey xor getKingSideCastlingKey(Black) + if self.castlingAvailability[Black].queen: + self.zobristKey = self.zobristKey xor getQueenSideCastlingKey(Black) + + if self.enPassantSquare != nullSquare(): + self.zobristKey = self.zobristKey xor getEnPassantKey(fileFromSquare(self.enPassantSquare)) + + +proc loadFEN*(fen: string): Position = + ## Initializes a position from the given + ## FEN string + result = Position(enPassantSquare: nullSquare()) + 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 + + # Make sure the mailbox is actually empty + for sq in Square(0)..Square(63): + result.mailbox[sq] = nullPiece() + + # 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.pieces[piece.color][piece.kind].setBit(square) + result.mailbox[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.sideToMove = White + of 'b': + result.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.castlingAvailability[White].king = true + of 'Q': + result.castlingAvailability[White].queen = true + of 'k': + result.castlingAvailability[Black].king = true + of 'q': + result.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.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.halfMoveClock = parseInt(s).uint16 + of 5: + # Fullmove number + var s = "" + while index <= fen.high(): + s.add(fen[index]) + inc(index) + result.fullMoveCount = parseInt(s).uint16 + else: + raise newException(ValueError, "invalid FEN: too many fields in FEN string") + inc(index) + result.updateChecksAndPins() + result.hash() + + +proc `$`*(self: Position): string = + result &= "- - - - - - - -" + var file = 8 + for i in 0..7: + result &= "\n" + for j in 0..7: + let piece = self.mailbox[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 toFEN*(self: Position): string = + ## Returns a FEN string of the + ## position + var skip: int + # Piece placement data + for i in 0..7: + skip = 0 + for j in 0..7: + let piece = self.mailbox[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.sideToMove == White: "w" else: "b") + result &= " " + # Castling availability + let castleWhite = self.castlingAvailability[White] + let castleBlack = self.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.enPassantSquare == nullSquare(): + result &= "-" + else: + result &= self.enPassantSquare.toAlgebraic() + result &= " " + # Halfmove clock + result &= $self.halfMoveClock + result &= " " + # Fullmove number + result &= $self.fullMoveCount + + +proc pretty*(self: Position): string = + ## Returns a colored version of the + ## position 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.mailbox[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" \ No newline at end of file diff --git a/Chess/nimfish/nimfishpkg/search.nim b/Chess/nimfish/nimfishpkg/search.nim index ec35871..647df8b 100644 --- a/Chess/nimfish/nimfishpkg/search.nim +++ b/Chess/nimfish/nimfishpkg/search.nim @@ -45,9 +45,10 @@ type currentExtensionCount: uint8 -proc newSearchManager*(board: Chessboard, transpositions: TTable): SearchManager = +proc newSearchManager*(position: Position, transpositions: TTable): SearchManager = new(result) - result.board = board + result.board = newChessboard() + result.board.position = position result.bestMoveRoot = nullMove() result.transpositionTable = transpositions @@ -79,7 +80,7 @@ proc getEstimatedMoveScore(self: SearchManager, move: Move): Score = # second goal we want to use our least valuable pieces to do so (this # is why we multiply the score of the captured piece by 10, to give # it priority) - result += 10 * self.board.getPieceScore(move.targetSquare) - self.board.getPieceScore(move.startSquare) + result += 10 * self.board.position.getPieceScore(move.targetSquare) - self.board.position.getPieceScore(move.startSquare) if move.isPromotion(): # Promotions are a good idea to search first var piece: Piece @@ -94,12 +95,12 @@ proc getEstimatedMoveScore(self: SearchManager, move: Move): Score = piece = Piece(kind: Queen, color: sideToMove) else: discard # Unreachable - result += self.board.getPieceScore(piece, move.targetSquare) - if self.board.getPawnAttacks(move.targetSquare, nonSideToMove) != 0: + result += self.board.position.getPieceScore(piece, move.targetSquare) + if self.board.position.getPawnAttacks(move.targetSquare, nonSideToMove) != 0: # Moving on a square attacked by an enemy pawn is _usually_ a very bad # idea. Assume the piece is lost and give a malus based on the fact that # losing a piece this way is dumb - result -= self.board.getPieceScore(move.startSquare) * 2 + result -= self.board.position.getPieceScore(move.startSquare) * 2 proc reorderMoves(self: SearchManager, moves: var MoveList) = @@ -152,7 +153,7 @@ proc getSearchExtension(self: SearchManager, move: Move): int {.used.} = # if we can do other interesting things! inc(self.currentExtensionCount) return 1 - let piece = self.board.getPiece(move.targetSquare) + let piece = self.board.position.getPiece(move.targetSquare) # If a pawn has just moved to its second-last rank, extend to # see if a promotion would yield some good position if piece.kind == Pawn: diff --git a/Chess/nimfish/nimfishpkg/tui.nim b/Chess/nimfish/nimfishpkg/tui.nim index 1fb28df..e04581b 100644 --- a/Chess/nimfish/nimfishpkg/tui.nim +++ b/Chess/nimfish/nimfishpkg/tui.nim @@ -56,7 +56,7 @@ proc perft*(board: var Chessboard, ply: int, verbose = false, divide = false, bu echo &"Ply (from root): {board.position.plyFromRoot}" echo &"Move: {move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}" echo &"Turn: {board.position.sideToMove}" - echo &"Piece: {board.getPiece(move.startSquare).kind}" + echo &"Piece: {board.position.getPiece(move.startSquare).kind}" echo &"Flags: {move.getFlags()}" echo &"In check: {(if board.inCheck(): \"yes\" else: \"no\")}" echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}" @@ -137,10 +137,10 @@ proc handleMoveCommand(board: var Chessboard, command: seq[string]): Move {.disc # we have to figure out all the flags by ourselves (whether it's a double # push, a capture, a promotion, etc.) - if board.getPiece(targetSquare).kind != Empty: + if board.position.getPiece(targetSquare).kind != Empty: flags.add(Capture) - if board.getPiece(startSquare).kind == Pawn and abs(rankFromSquare(startSquare) - rankFromSquare(targetSquare)) == 2: + if board.position.getPiece(startSquare).kind == Pawn and abs(rankFromSquare(startSquare) - rankFromSquare(targetSquare)) == 2: flags.add(DoublePush) if len(moveString) == 5: @@ -160,12 +160,13 @@ proc handleMoveCommand(board: var Chessboard, command: seq[string]): Move {.disc var move = createMove(startSquare, targetSquare, flags) - let piece = board.getPiece(move.startSquare) + let piece = board.position.getPiece(move.startSquare) if piece.kind == King and move.startSquare == board.position.sideToMove.getKingStartingSquare(): if move.targetSquare in [piece.kingSideCastling(), piece.queenSideCastling()]: move.flags = move.flags or Castle.uint16 - elif move.targetSquare == board.position.enPassantSquare: - move.flags = move.flags or EnPassant.uint16 + elif piece.kind == Pawn and targetSquare == board.position.enPassantSquare: + # I hate en passant I hate en passant I hate en passant I hate en passant I hate en passant I hate en passant + flags.add(EnPassant) result = board.makeMove(move) if result == nullMove(): echo &"Error: move: {moveString} is illegal" @@ -343,7 +344,8 @@ const HELP_TEXT = """Nimfish help menu: - fen: Shorthand for "position fen" - pos : Shorthand for "position " - get : Get the piece on the given square - - atk : Print which pieces are currently attacking the given square + - atk : Print which opponent pieces are attacking the given square + - def : Print which friendly pieces are attacking the given square - pins: Print the current pin masks, if any - checks: Print the current check mask, if in check - skip: Make a null move (i.e. pass your turn). Useful for debugging. Very much illegal @@ -381,7 +383,7 @@ proc commandLoop*: int = echo HELP_TEXT of "skip": board.position.sideToMove = board.position.sideToMove.opposite() - board.updateChecksAndPins() + board.position.updateChecksAndPins() of "go": handleGoCommand(board, cmd) of "position", "pos": @@ -402,10 +404,19 @@ proc commandLoop*: int = echo "error: atk: invalid number of arguments" continue try: - echo board.getAttacksTo(cmd[1].toSquare(), board.position.sideToMove.opposite()) + echo board.position.getAttacksTo(cmd[1].toSquare(), board.position.sideToMove.opposite()) except ValueError: echo "error: atk: invalid square" continue + of "def": + if len(cmd) != 2: + echo "error: def: invalid number of arguments" + continue + try: + echo board.position.getAttacksTo(cmd[1].toSquare(), board.position.sideToMove) + except ValueError: + echo "error: def: invalid square" + continue of "ep": let target = board.position.enPassantSquare if target != nullSquare(): @@ -417,7 +428,7 @@ proc commandLoop*: int = echo "error: get: invalid number of arguments" continue try: - echo board.getPiece(cmd[1]) + echo board.position.getPiece(cmd[1]) except ValueError: echo "error: get: invalid square" continue diff --git a/Chess/nimfish/nimfishpkg/uci.nim b/Chess/nimfish/nimfishpkg/uci.nim index 304090a..09a1258 100644 --- a/Chess/nimfish/nimfishpkg/uci.nim +++ b/Chess/nimfish/nimfishpkg/uci.nim @@ -89,10 +89,10 @@ proc parseUCIMove(session: UCISession, move: string): tuple[move: Move, command: # Since the client tells us just the source and target square of the move, # we have to figure out all the flags by ourselves (whether it's a double # push, a capture, a promotion, etc.) - if session.board.getPiece(targetSquare).kind != Empty: + if session.board.position.getPiece(targetSquare).kind != Empty: flags.add(Capture) - if session.board.getPiece(startSquare).kind == Pawn and abs(rankFromSquare(startSquare) - rankFromSquare(targetSquare)) == 2: + if session.board.position.getPiece(startSquare).kind == Pawn and abs(rankFromSquare(startSquare) - rankFromSquare(targetSquare)) == 2: flags.add(DoublePush) if len(move) == 5: @@ -108,7 +108,7 @@ proc parseUCIMove(session: UCISession, move: string): tuple[move: Move, command: flags.add(PromoteToRook) else: return - let piece = session.board.getPiece(startSquare) + let piece = session.board.position.getPiece(startSquare) if piece.kind == King and startSquare == session.board.position.sideToMove.getKingStartingSquare(): if targetSquare in [piece.kingSideCastling(), piece.queenSideCastling()]: flags.add(Castle) @@ -303,7 +303,7 @@ proc bestMove(args: tuple[session: UCISession, command: UCICommand]) {.thread.} echo &"info string created {session.hashTableSize} MiB TT" session.transpositionTable = newTranspositionTable(session.hashTableSize * 1024 * 1024) var command = args.command - var searcher = newSearchManager(session.board, session.transpositionTable) + var searcher = newSearchManager(session.board.position, session.transpositionTable) session.currentSearch.store(searcher) var timeRemaining = (if session.board.position.sideToMove == White: command.wtime else: command.btime)