From 6115191ed6a875037d29cd234147e54de426b753 Mon Sep 17 00:00:00 2001 From: nocturn9x Date: Mon, 15 Apr 2024 12:04:50 +0200 Subject: [PATCH] Refactoring and more work on bitboard handling --- src/Chess/board.nim | 989 ++++++++++++++++++++++++++------------------ 1 file changed, 590 insertions(+), 399 deletions(-) diff --git a/src/Chess/board.nim b/src/Chess/board.nim index 55ffbce..a8170be 100644 --- a/src/Chess/board.nim +++ b/src/Chess/board.nim @@ -16,6 +16,7 @@ import std/strutils import std/strformat import std/times import std/math +import std/bitops type @@ -36,6 +37,18 @@ type Queen = 'q', Rook = 'r', + Direction* = enum + ## A move direction enumeration + Forward, + Backward, + Left, + Right + ForwardLeft, + ForwardRight, + BackwardLeft, + BackwardRight + + Piece* = object ## A chess piece color*: PieceColor @@ -56,25 +69,25 @@ type PromoteToBishop = 128, PromoteToKnight = 256 - # Useful type aliases - Location* = tuple[row, col: int8] - - Attacked = seq[tuple[source, target, direction: Location]] + Bitboard* = distinct uint64 - Pieces = tuple[king: Location, queens: seq[Location], rooks: seq[Location], - bishops: seq[Location], knights: seq[Location], - pawns: seq[Location]] + Square* = tuple[rank, file: int8] + + Attacked = seq[tuple[source, target, direction: Square]] + + Pieces = tuple[king: Square, queens: seq[Square], rooks: seq[Square], + bishops: seq[Square], knights: seq[Square], + pawns: seq[Square]] CountData = tuple[nodes: uint64, captures: uint64, castles: uint64, checks: uint64, promotions: uint64, enPassant: uint64, checkmates: uint64] Move* = object ## A chess move - startSquare*: Location - targetSquare*: Location + startSquare*: Square + targetSquare*: Square flags*: uint16 - - Position* = ref object + Position* = object ## A chess position # Did the rooks on either side or the king move? castlingAvailable: tuple[white, black: tuple[queen, king: bool]] @@ -90,16 +103,20 @@ type # every 2 ply fullMoveCount: int8 # En passant target square (see https://en.wikipedia.org/wiki/En_passant) - enPassantSquare*: Location - # Locations of all pieces - pieces: tuple[white: Pieces, black: Pieces] + enPassantSquare*: Square + # Squares of all pieces + pieces: tuple[white, black: Pieces] # Squares attacked by both sides - attacked: tuple[white: Attacked, black: Attacked] + attacked: tuple[white, black: Attacked] # Pieces pinned by both sides (only absolute pins) - pinned: tuple[white: Attacked, black: Attacked] + pinned: tuple[white, black: Attacked] # Active color turn: PieceColor + # Bitboards for all pieces + bitboards: tuple[white, black: tuple[king, queens, rooks, bishops, knights, pawns: Bitboard]] + + ChessBoard* = ref object ## A chess board object @@ -115,34 +132,34 @@ type # A bunch of simple utility functions and forward declarations func emptyPiece*: Piece {.inline.} = Piece(kind: Empty, color: None) -func emptyLocation*: Location {.inline.} = (-1 , -1) +func emptySquare*: Square {.inline.} = (-1 , -1) func opposite*(c: PieceColor): PieceColor {.inline.} = (if c == White: Black else: White) -proc algebraicToLocation*(s: string): Location {.inline.} +proc algebraicToSquare*(s: string): Square {.inline.} proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} -func emptyMove*: Move {.inline.} = Move(startSquare: emptyLocation(), targetSquare: emptyLocation()) -func `+`*(a, b: Location): Location = (a.row + b.row, a.col + b.col) -func `-`*(a: Location): Location = (-a.row, -a.col) -func `-`*(a, b: Location): Location = (a.row - b.row, a.col - b.col) -func isValid*(a: Location): bool {.inline.} = a.row in 0..7 and a.col in 0..7 -func isLightSquare(a: Location): bool {.inline.} = (a.row + a.col and 2) == 0 -proc generateMoves(self: ChessBoard, location: Location): seq[Move] -proc getAttackers*(self: ChessBoard, loc: Location, color: PieceColor): seq[Location] -proc getAttackFor*(self: ChessBoard, source, target: Location): tuple[source, target, direction: Location] -proc isAttacked*(self: ChessBoard, loc: Location, color: PieceColor = None): bool +func emptyMove*: Move {.inline.} = Move(startSquare: emptySquare(), targetSquare: emptySquare()) +func `+`*(a, b: Square): Square = (a.rank + b.rank, a.file + b.file) +func `-`*(a: Square): Square = (-a.rank, -a.file) +func `-`*(a, b: Square): Square = (a.rank - b.rank, a.file - b.file) +func isValid*(a: Square): bool {.inline.} = a.rank in 0..7 and a.file in 0..7 +func isLightSquare(a: Square): bool {.inline.} = (a.rank + a.file and 2) == 0 +proc generateMoves(self: ChessBoard, square: Square): seq[Move] +proc getAttackers*(self: ChessBoard, square: Square, color: PieceColor): seq[Square] +proc getAttackFor*(self: ChessBoard, source, target: Square): tuple[source, target, direction: Square] +proc isAttacked*(self: ChessBoard, square: Square, color: PieceColor = None): bool proc isLegal(self: ChessBoard, move: Move): bool {.inline.} proc doMove(self: ChessBoard, move: Move) proc pretty*(self: ChessBoard): string -proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) +proc spawnPiece(self: ChessBoard, square: Square, piece: Piece) proc updateAttackedSquares(self: ChessBoard) proc updateSlidingAttacks(self: ChessBoard) -proc getPinnedDirections(self: ChessBoard, loc: Location): seq[Location] -proc getAttacks*(self: ChessBoard, loc: Location): Attacked -proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked, pins: Attacked] +proc getPinnedDirections(self: ChessBoard, square: Square): seq[Square] +proc getAttacks*(self: ChessBoard, square: Square): Attacked +proc getSlidingAttacks(self: ChessBoard, square: Square): tuple[attacks: Attacked, pins: Attacked] proc inCheck*(self: ChessBoard, color: PieceColor = None): bool proc toFEN*(self: ChessBoard): string proc undoLastMove*(self: ChessBoard) proc movePiece(self: ChessBoard, move: Move, attack: bool = true) -proc removePiece(self: ChessBoard, location: Location, attack: bool = true) +proc removePiece(self: ChessBoard, square: Square, attack: bool = true) proc extend[T](self: var seq[T], other: openarray[T]) {.inline.} = @@ -151,22 +168,23 @@ proc extend[T](self: var seq[T], other: openarray[T]) {.inline.} = proc updateBoard*(self: ChessBoard) + # Due to our board layout, directions of movement are reversed for white and black, so # we need these helpers to avoid going mad with integer tuples and minus signs everywhere -func topLeftDiagonal(color: PieceColor): Location {.inline.} = (if color == White: (-1, -1) else: (1, 1)) -func topRightDiagonal(color: PieceColor): Location {.inline.} = (if color == White: (-1, 1) else: (1, -1)) -func bottomLeftDiagonal(color: PieceColor): Location {.inline.} = (if color == White: (1, -1) else: (-1, 1)) -func bottomRightDiagonal(color: PieceColor): Location {.inline.} = (if color == White: (1, 1) else: (-1, -1)) -func leftSide(color: PieceColor): Location {.inline.} = (if color == White: (0, -1) else: (0, 1)) -func rightSide(color: PieceColor): Location {.inline.} = (if color == White: (0, 1) else: (0, -1)) -func topSide(color: PieceColor): Location {.inline.} = (if color == White: (-1, 0) else: (1, 0)) -func bottomSide(color: PieceColor): Location {.inline.} = (if color == White: (1, 0) else: (-1, 0)) -func doublePush(color: PieceColor): Location {.inline.} = (if color == White: (-2, 0) else: (2, 0)) -func longCastleKing: Location {.inline.} = (0, -2) -func shortCastleKing: Location {.inline.} = (0, 2) -func longCastleRook: Location {.inline.} = (0, 3) -func shortCastleRook: Location {.inline.} = (0, -2) -func bottomLeftKnightMove(color: PieceColor, long: bool = true): Location {.inline.} = +func topLeftDiagonal(color: PieceColor): Square {.inline.} = (if color == White: (-1, -1) else: (1, 1)) +func topRightDiagonal(color: PieceColor): Square {.inline.} = (if color == White: (-1, 1) else: (1, -1)) +func bottomLeftDiagonal(color: PieceColor): Square {.inline.} = (if color == White: (1, -1) else: (-1, 1)) +func bottomRightDiagonal(color: PieceColor): Square {.inline.} = (if color == White: (1, 1) else: (-1, -1)) +func leftSide(color: PieceColor): Square {.inline.} = (if color == White: (0, -1) else: (0, 1)) +func rightSide(color: PieceColor): Square {.inline.} = (if color == White: (0, 1) else: (0, -1)) +func topSide(color: PieceColor): Square {.inline.} = (if color == White: (-1, 0) else: (1, 0)) +func bottomSide(color: PieceColor): Square {.inline.} = (if color == White: (1, 0) else: (-1, 0)) +func doublePush(color: PieceColor): Square {.inline.} = (if color == White: (-2, 0) else: (2, 0)) +func longCastleKing: Square {.inline.} = (0, -2) +func shortCastleKing: Square {.inline.} = (0, 2) +func longCastleRook: Square {.inline.} = (0, 3) +func shortCastleRook: Square {.inline.} = (0, -2) +func bottomLeftKnightMove(color: PieceColor, long: bool = true): Square {.inline.} = if color == White: if long: return (2, -1) @@ -179,7 +197,7 @@ func bottomLeftKnightMove(color: PieceColor, long: bool = true): Location {.inli return (1, -2) -func bottomRightKnightMove(color: PieceColor, long: bool = true): Location {.inline.} = +func bottomRightKnightMove(color: PieceColor, long: bool = true): Square {.inline.} = if color == White: if long: return (2, 1) @@ -192,7 +210,7 @@ func bottomRightKnightMove(color: PieceColor, long: bool = true): Location {.inl return (-1, -2) -func topLeftKnightMove(color: PieceColor, long: bool = true): Location {.inline.} = +func topLeftKnightMove(color: PieceColor, long: bool = true): Square {.inline.} = if color == White: if long: return (-2, -1) @@ -205,7 +223,7 @@ func topLeftKnightMove(color: PieceColor, long: bool = true): Location {.inline. return (1, 2) -func topRightKnightMove(color: PieceColor, long: bool = true): Location {.inline.} = +func topRightKnightMove(color: PieceColor, long: bool = true): Square {.inline.} = if color == White: if long: return (-2, 1) @@ -217,9 +235,9 @@ func topRightKnightMove(color: PieceColor, long: bool = true): Location {.inline else: return (-1, 2) -# These return absolute locations rather than relative direction offsets -func kingSideRook(color: PieceColor): Location {.inline.} = (if color == White: (7, 7) else: (0, 7)) -func queenSideRook(color: PieceColor): Location {.inline.} = (if color == White: (7, 0) else: (0, 0)) +# These return absolute squares rather than relative direction offsets +func kingSideRook(color: PieceColor): Square {.inline.} = (if color == White: (7, 7) else: (0, 7)) +func queenSideRook(color: PieceColor): Square {.inline.} = (if color == White: (7, 0) else: (0, 0)) # A bunch of getters @@ -229,7 +247,7 @@ func getActiveColor*(self: ChessBoard): PieceColor {.inline.} = return self.position.turn -func getEnPassantTarget*(self: ChessBoard): Location {.inline.} = +func getEnPassantTarget*(self: ChessBoard): Square {.inline.} = ## Returns the current en passant target square return self.position.enPassantSquare @@ -246,7 +264,7 @@ func getHalfMoveCount*(self: ChessBoard): int {.inline.} = return self.position.halfMoveClock -func getStartRow(piece: Piece): int {.inline.} = +func getStartRank(piece: Piece): int {.inline.} = ## Retrieves the starting row of ## the given piece inside our 8x8 ## grid @@ -267,8 +285,8 @@ func getStartRow(piece: Piece): int {.inline.} = return 0 -func getKingStartingPosition(color: PieceColor): Location {.inline.} = - ## Retrieves the starting location of the king +func getKingStartingPosition(color: PieceColor): Square {.inline.} = + ## Retrieves the starting square of the king ## for the given color case color: of White: @@ -279,9 +297,9 @@ func getKingStartingPosition(color: PieceColor): Location {.inline.} = discard -func getLastRow(color: PieceColor): int {.inline.} = - ## Retrieves the location of the last - ## row relative to the given color +func getLastRank(color: PieceColor): int {.inline.} = + ## Retrieves the square of the last + ## rank relative to the given color case color: of White: return 0 @@ -297,28 +315,166 @@ proc newChessboard: ChessBoard = for i in 0..63: result.grid[i] = emptyPiece() result.position = Position(attacked: (@[], @[]), - enPassantSquare: emptyLocation(), + enPassantSquare: emptySquare(), turn: White, fullMoveCount: 1, - pieces: (white: (king: emptyLocation(), + pieces: (white: (king: emptySquare(), queens: @[], rooks: @[], bishops: @[], knights: @[], pawns: @[]), - black: (king: emptyLocation(), + black: (king: emptySquare(), queens: @[], rooks: @[], bishops: @[], knights: @[], pawns: @[]))) +# Handy wrappers and utilities for handling low-level stuff +func coordToIndex(row, col: SomeInteger): int {.inline.} = (row * 8) + col +func coordToIndex(square: Square): int {.inline.} = coordToIndex(square.rank, square.file) +func indexToCoord(index: SomeInteger): Square {.inline.} = (index / 8, index mod 8) -func coordToIndex(row, col: int): int {.inline.} = (row * 8) + col +# Indexing operations func `[]`(self: array[64, Piece], row, column: Natural): Piece {.inline.} = self[coordToIndex(row, column)] proc `[]=`(self: var array[64, Piece], row, column: Natural, piece: Piece) {.inline.} = self[coordToIndex(row, column)] = piece -func `[]`(self: array[64, Piece], loc: Location): Piece {.inline.} = self[loc.row, loc.col] -proc `[]=`(self: var array[64, Piece], loc: Location, piece: Piece) {.inline.} = self[loc.row, loc.col] = piece +func `[]`(self: array[64, Piece], square: Square): Piece {.inline.} = self[square.rank, square.file] +func `[]=`(self: var array[64, Piece], square: Square, piece: Piece) {.inline.} = self[square.rank, square.file] = piece + +# Overloaded operators and functions for our bitboard type +func `shl`(a: Bitboard, x: Positive): Bitboard = Bitboard(a.uint64 shl x) +func `shr`(a: Bitboard, x: Positive): Bitboard = Bitboard(a.uint64 shr x) +func `and`(a, b: Bitboard): Bitboard = Bitboard(a.uint64 and b.uint64) +func `shr`(a, b: Bitboard): Bitboard = Bitboard(a.uint64 and b.uint64) +func `==`(a, b: Bitboard): bool {.inline.} = a.uint64 == b.uint64 +func `==`(a: Bitboard, b: SomeInteger): bool {.inline.} = a.uint64 == b.uint64 + + +func getFileMask(file: Positive): Bitboard = Bitboard(0x101010101010101'u64) shl file +func getRankMask(rank: Positive): Bitboard = Bitboard(uint64.high()) shl Positive(8 * (rank + 1)) +func squareToBitboard(square: SomeInteger): Bitboard = Bitboard(1'u64) shl square.uint64 +func squareToBitboard(square: Square): Bitboard = squareToBitboard(coordToIndex(square)) + +func toBin(x: Bitboard, b: Positive = 64): string = toBin(BiggestInt(x), b) +func toBin(x: uint64, b: Positive = 64): string = toBin(Bitboard(x), b) + + +func computeDiagonalBitboards: array[14, Bitboard] {.compileTime.} = + ## Precomputes all the bitboards for diagonals + ## at compile time + result[0] = Bitboard(0x8040201008040201'u64) + var + col = 1 + i = 0 + # Left to right + while col < 8: + result[col] = Bitboard(0x8040201008040201'u64) shl (8 * col) + inc(col) + inc(i) + result[i] = Bitboard(0x102040810204080'u64) + inc(i) + col = 1 + # Right to left + while col < 7: + result[i] = Bitboard(0x102040810204080'u64) shr (8 * col) + inc(i) + inc(col) + + +const diagonalBitboards = computeDiagonalBitboards() + + +func getDirectionMask(square: Square, color: PieceColor, direction: Direction): Bitboard = + ## Get a bitmask for the given direction for a piece + ## of the given color + case color: + of White: + case direction: + of Forward: + return squareToBitboard(square) shl 8 + of Backward: + return squareToBitboard(square) shr 8 + of ForwardRight: + return squareToBitboard(square) shl 9 + of ForwardLeft: + return squareToBitboard(square) shr 9 + of BackwardRight: + return squareToBitboard(square) shl 17 + of BackwardLeft: + return squareToBitboard(square) shr 17 + else: + discard + of Black: + # The directions for black are just the opposite of those for white, + # so we avoid duplicating any code + case direction: + of Forward: + return getDirectionMask(square, White, Backward) + of Backward: + return getDirectionMask(square, White, Forward) + of ForwardRight: + return getDirectionMask(square, White, ForwardLeft) + of ForwardLeft: + return getDirectionMask(square, White, ForwardRight) + of BackwardRight: + return getDirectionMask(square, White, BackwardLeft) + of BackwardLeft: + return getDirectionMask(square, White, BackwardRight) + else: + discard + else: + discard + + +func getDirectionMask(self: ChessBoard, square: Square, direction: Direction): Bitboard = + ## Like getDirectionMask(), but used within the board context + ## with a piece square and direction only + return getDirectionMask(square, self.grid[square].color, direction) + + +func getBitboard(self: ChessBoard, kind: PieceKind, color: PieceColor): Bitboard = + ## Returns the positional bitboard for the given piece kind and color + case color: + of White: + case kind: + of Pawn: + return self.position.bitboards.white.pawns + of Knight: + return self.position.bitboards.white.knights + of Bishop: + return self.position.bitboards.white.bishops + of Rook: + return self.position.bitboards.white.rooks + of Queen: + return self.position.bitboards.white.queens + of King: + return self.position.bitboards.white.king + else: + discard + of Black: + case kind: + of Pawn: + return self.position.bitboards.black.pawns + of Knight: + return self.position.bitboards.black.knights + of Bishop: + return self.position.bitboards.black.bishops + of Rook: + return self.position.bitboards.black.rooks + of Queen: + return self.position.bitboards.black.queens + of King: + return self.position.bitboards.black.king + else: + discard + else: + discard + + +func getBitboard(self: ChessBoard, piece: Piece): Bitboard = + ## Returns the positional bitboard for the given piece type + return self.getBitboard(piece.kind, piece.color) proc newChessboardFromFEN*(fen: string): ChessBoard = @@ -326,7 +482,7 @@ proc newChessboardFromFEN*(fen: string): ChessBoard = ## position encoded by the given FEN string result = newChessboard() var - # Current location in the grid + # Current square in the grid row: int8 = 0 column: int8 = 0 # Current section in the FEN string @@ -335,6 +491,7 @@ proc newChessboardFromFEN*(fen: string): ChessBoard = index = 0 # Temporary variable to store a piece piece: Piece + pieces: int # See https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation while index <= fen.high(): var c = fen[index] @@ -349,6 +506,9 @@ proc newChessboardFromFEN*(fen: string): ChessBoard = case c.toLowerAscii(): # Piece of 'r', 'n', 'b', 'q', 'k', 'p': + let + square: Square = (row, column) + bitIndex = square.coordToIndex() # We know for a fact these values are in our # enumeration, so all is good {.warning[HoleEnumConv]:off.} @@ -357,42 +517,42 @@ proc newChessboardFromFEN*(fen: string): ChessBoard = of Black: case piece.kind: of Pawn: - result.position.pieces.black.pawns.add((row, column)) + result.position.bitboards.black.pawns.uint64.uint64.setBit(bitIndex) of Bishop: - result.position.pieces.black.bishops.add((row, column)) + result.position.bitboards.black.bishops.uint64.setBit(bitIndex) of Knight: - result.position.pieces.black.knights.add((row, column)) + result.position.bitboards.black.knights.uint64.setBit(bitIndex) of Rook: - result.position.pieces.black.rooks.add((row, column)) + result.position.bitboards.black.rooks.uint64.setBit(bitIndex) of Queen: - result.position.pieces.black.queens.add((row, column)) + result.position.bitboards.black.queens.uint64.setBit(bitIndex) of King: - if result.position.pieces.black.king != emptyLocation(): + if result.position.bitboards.black.king != Bitboard(0'u64): raise newException(ValueError, "invalid position: exactly one king of each color must be present") - result.position.pieces.black.king = (row, column) + result.position.bitboards.black.king.uint64.setBit(bitIndex) else: discard of White: case piece.kind: of Pawn: - result.position.pieces.white.pawns.add((row, column)) + result.position.bitboards.white.pawns.uint64.setBit(bitIndex) of Bishop: - result.position.pieces.white.bishops.add((row, column)) + result.position.bitboards.white.bishops.uint64.setBit(bitIndex) of Knight: - result.position.pieces.white.knights.add((row, column)) + result.position.bitboards.white.knights.uint64.setBit(bitIndex) of Rook: - result.position.pieces.white.rooks.add((row, column)) + result.position.bitboards.white.rooks.uint64.setBit(bitIndex) of Queen: - result.position.pieces.white.queens.add((row, column)) + result.position.bitboards.white.queens.uint64.setBit(bitIndex) of King: - if result.position.pieces.white.king != emptyLocation(): + if result.position.bitboards.white.king != 0: raise newException(ValueError, "invalid position: exactly one king of each color must be present") - result.position.pieces.white.king = (row, column) + result.position.bitboards.white.king.uint64.setBit(bitIndex) else: discard else: discard - result.grid[row, column] = piece + result.grid[square] = piece inc(column) of '/': # Next row @@ -440,7 +600,7 @@ proc newChessboardFromFEN*(fen: string): ChessBoard = # Field is already uninitialized to the correct state discard else: - result.position.enPassantSquare = fen[index..index+1].algebraicToLocation() + result.position.enPassantSquare = fen[index..index+1].algebraicToSquare() # Square metadata is 2 bytes long inc(index) of 4: @@ -467,7 +627,7 @@ proc newChessboardFromFEN*(fen: string): ChessBoard = if result.inCheck(result.getActiveColor().opposite): # Opponent king cannot be captured on the next move raise newException(ValueError, "invalid position: opponent king can be captured") - if result.position.pieces.white.king == emptyLocation() or result.position.pieces.black.king == emptyLocation(): + if result.position.bitboards.white.king == Bitboard(0) or result.position.bitboards.black.king == Bitboard(0): # Both kings must be on the board raise newException(ValueError, "invalid position: exactly one king of each color must be present") @@ -486,35 +646,33 @@ proc countPieces*(self: ChessBoard, kind: PieceKind, color: PieceColor): int = of White: case kind: of Pawn: - return self.position.pieces.white.pawns.len() + return self.position.bitboards.white.pawns.uint64.countSetBits() of Bishop: - return self.position.pieces.white.bishops.len() + return self.position.bitboards.white.bishops.uint64.countSetBits() of Knight: - return self.position.pieces.white.knights.len() + return self.position.bitboards.white.knights.uint64.countSetBits() of Rook: - return self.position.pieces.white.rooks.len() + return self.position.bitboards.white.rooks.uint64.countSetBits() of Queen: - return self.position.pieces.white.queens.len() + return self.position.bitboards.white.queens.uint64.countSetBits() of King: - # There shall be only one, forever - return 1 + return self.position.bitboards.white.king.uint64.countSetBits() else: - raise newException(ValueError, "invalid piece type") + raise newException(ValueError, "invalid piece type") of Black: case kind: of Pawn: - return self.position.pieces.black.pawns.len() + return self.position.bitboards.black.pawns.uint64.countSetBits() of Bishop: - return self.position.pieces.black.bishops.len() + return self.position.bitboards.black.bishops.uint64.countSetBits() of Knight: - return self.position.pieces.black.knights.len() + return self.position.bitboards.black.knights.uint64.countSetBits() of Rook: - return self.position.pieces.black.rooks.len() + return self.position.bitboards.black.rooks.uint64.countSetBits() of Queen: - return self.position.pieces.black.queens.len() + return self.position.bitboards.black.queens.uint64.countSetBits() of King: - # In perpetuity - return 1 + return self.position.bitboards.black.king.uint64.countSetBits() else: raise newException(ValueError, "invalid piece type") of None: @@ -544,8 +702,8 @@ func rowToFile(row: int): int8 {.inline.} = return indeces[row] -proc algebraicToLocation*(s: string): Location = - ## Converts a square location from algebraic +proc algebraicToSquare*(s: string): Square = + ## Converts a square square from algebraic ## notation to its corresponding row and column ## in the chess grid (0 indexed) if len(s) != 2: @@ -563,21 +721,21 @@ proc algebraicToLocation*(s: string): Location = return (file, rank) -func locationToAlgebraic*(loc: Location): string {.inline.} = - ## Converts a location from our internal row, column +func squareToAlgebraic*(square: Square): string {.inline.} = + ## Converts a square from our internal row, column ## notation to a square in algebraic notation - return &"{char(uint8(loc.col) + uint8('a'))}{rowToFile(loc.row)}" + return &"{char(uint8(square.file) + uint8('a'))}{rowToFile(square.rank)}" -func getPiece*(self: ChessBoard, loc: Location): Piece {.inline.} = - ## Gets the piece at the given location - return self.grid[loc.row, loc.col] +func getPiece*(self: ChessBoard, square: Square): Piece {.inline.} = + ## Gets the piece at the given square + return self.grid[square.rank, square.file] func getPiece*(self: ChessBoard, square: string): Piece {.inline.} = ## Gets the piece on the given square ## in algebraic notation - return self.getPiece(square.algebraicToLocation()) + return self.getPiece(square.algebraicToSquare()) func isPromotion*(move: Move): bool {.inline.} = @@ -643,8 +801,8 @@ func getFlags*(move: Move): seq[MoveFlag] = result.add(Default) -func getKing(self: ChessBoard, color: PieceColor = None): Location {.inline.} = - ## Returns the location of the king for the given +func getKing(self: ChessBoard, color: PieceColor = None): Square {.inline.} = + ## Returns the square of the king for the given ## color (if it is None, the active color is used) var color = color if color == None: @@ -724,19 +882,19 @@ proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: return (false, false) if result.king or result.queen: var - loc: Location - queenSide: Location - kingSide: Location + square: Square + queenSide: Square + kingSide: Square # If the path between the king and rook on a given side is blocked, or any of the # squares where the king would move to are attacked by the opponent, then castling # is temporarily prohibited on that side case color: of White: - loc = self.position.pieces.white.king + square = self.position.pieces.white.king queenSide = color.leftSide() kingSide = color.rightSide() of Black: - loc = self.position.pieces.black.king + square = self.position.pieces.black.king queenSide = color.rightSide() kingSide = color.leftSide() of None: @@ -755,57 +913,57 @@ proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: if result.king: # Short castle var - location = loc + currentSquare = square otherPiece: Piece while true: - location = location + kingSide + currentSquare = currentSquare + kingSide - if location == color.kingSideRook(): + if currentSquare == color.kingSideRook(): break - otherPiece = self.grid[location.row, location.col] + otherPiece = self.grid[currentSquare.rank, currentSquare.file] if otherPiece.color != None: result.king = false break - if checkAttacks and self.isAttacked(location, color.opposite()): + if checkAttacks and self.isAttacked(currentSquare, color.opposite()): result.king = false break # King has arrived at the target square: we no longer # need to check whether subsequent squares are free from # attacks - if location == shortCastleKing() + loc: + if currentSquare == shortCastleKing() + square: checkAttacks = false if result.queen: checkAttacks = true # Long castle var - location = loc + currentSquare = square otherPiece: Piece while true: - location = location + queenSide + currentSquare = currentSquare + queenSide - if location == color.queenSideRook(): + if currentSquare == color.queenSideRook(): break - otherPiece = self.grid[location.row, location.col] + otherPiece = self.grid[currentSquare.rank, currentSquare.file] if otherPiece.color != None: result.queen = false break - if checkAttacks and self.isAttacked(location, color.opposite()): + if checkAttacks and self.isAttacked(currentSquare, color.opposite()): result.queen = false break - if location == longCastleKing() + loc: + if currentSquare == longCastleKing() + square: checkAttacks = false -proc getCheckResolutions(self: ChessBoard, color: PieceColor): seq[Location] = +proc getCheckResolutions(self: ChessBoard, color: PieceColor): seq[Square] = ## Returns the squares that need to be covered to ## resolve the current check (including capturing ## the checking piece). In case of double check, an @@ -814,7 +972,7 @@ proc getCheckResolutions(self: ChessBoard, color: PieceColor): seq[Location] = ## case of a friendly pawn being able to capture an enemy ## pawn that is checking our friendly king via en passant: ## that is handled internally by generatePawnMoves - var king: Location + var king: Square case color: of White: king = self.position.pieces.white.king @@ -823,13 +981,13 @@ proc getCheckResolutions(self: ChessBoard, color: PieceColor): seq[Location] = else: return - let attackers: seq[Location] = self.getAttackers(king, color.opposite()) + let attackers: seq[Square] = self.getAttackers(king, color.opposite()) if attackers.len() > 1: # Double checks require to move the king return @[] let attacker = attackers[0] - attackerPiece = self.grid[attacker.row, attacker.col] + attackerPiece = self.grid[attacker] var attack = self.getAttackFor(attacker, king) # Capturing the piece resolves the check @@ -840,29 +998,29 @@ proc getCheckResolutions(self: ChessBoard, color: PieceColor): seq[Location] = # in which case either the king has to move or # that piece has to be captured, but this is # already implicitly handled by the loop below) - var location = attacker - while location != king: - location = location + attack.direction - if not location.isValid(): + var square = attacker + while square != king: + square = square + attack.direction + if not square.isValid(): break - result.add(location) + result.add(square) -proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = +proc generatePawnMoves(self: ChessBoard, square: Square): seq[Move] = ## Generates the possible moves for the pawn in the given - ## location + ## square var - piece = self.grid[location.row, location.col] - directions: seq[Location] = @[] + piece = self.grid[square.rank, square.file] + directions: seq[Square] = @[] assert piece.kind == Pawn, &"generatePawnMoves called on a {piece.kind}" # Pawns can move forward one square - let forward = location + piece.color.topSide() + let forward = square + piece.color.topSide() # Only if the square is empty though if forward.isValid() and self.grid[forward].color == None: directions.add(piece.color.topSide()) # If the pawn is on its first rank, it can push two squares - if location.row == piece.getStartRow(): - let double = location + piece.color.doublePush() + if square.rank == piece.getStartRank(): + let double = square + piece.color.doublePush() # Check that both squares are empty if double.isValid() and self.grid[forward].color == None and self.grid[double].color == None: directions.add(piece.color.doublePush()) @@ -875,7 +1033,7 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = # They can also move one square on either of their # forward diagonals, but only for captures and en passant for diagonal in [topRight, topLeft]: - let target = location + diagonal + let target = square + diagonal if target.isValid(): let otherPiece = self.grid[target] if target == enPassantTarget and self.grid[enPassantPawn].color == piece.color.opposite(): @@ -883,7 +1041,7 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = let targetPawn = self.grid[enPassantPawn] # Simulate the move and see if the king ends up in check self.removePiece(enPassantPawn, attack=false) - self.removePiece(location, attack=false) + self.removePiece(square, attack=false) self.spawnPiece(target, piece) self.updateAttackedSquares() if not self.inCheck(piece.color): @@ -892,16 +1050,16 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = enPassantLegal = true # Reset what we just did and reupdate the attack metadata self.removePiece(target, attack=false) - self.spawnPiece(location, piece) + self.spawnPiece(square, piece) self.spawnPiece(enPassantPawn, targetPawn) self.updateAttackedSquares() elif otherPiece.color == piece.color.opposite() and otherPiece.kind != King: # Can't capture the king! # A capture may be possible directions.add(diagonal) # Check for pins - let pinned = self.getPinnedDirections(location) + let pinned = self.getPinnedDirections(square) if pinned.len() > 0: - var newDirections: seq[Location] = @[] + var newDirections: seq[Square] = @[] for direction in directions: if direction in pinned: newDirections.add(direction) @@ -916,30 +1074,30 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = resolutions.add(enPassantTarget) var targetPiece: Piece for direction in directions: - let target = location + direction + let target = square + direction if checked and target notin resolutions: continue targetPiece = self.grid[target] var flags: uint16 = Default.uint16 if targetPiece.color != None: flags = flags or Capture.uint16 - elif abs(location.row - target.row) == 2: + elif abs(square.rank - target.rank) == 2: flags = flags or DoublePush.uint16 elif target == self.getEnPassantTarget(): flags = flags or EnPassant.uint16 - if target.row == piece.color.getLastRow(): + if target.rank == piece.color.getLastRank(): # Pawn reached the other side of the board: generate all potential piece promotions for promotionType in [PromoteToKnight, PromoteToBishop, PromoteToRook, PromoteToQueen]: - result.add(Move(startSquare: location, targetSquare: target, flags: promotionType.uint16 or flags)) + result.add(Move(startSquare: square, targetSquare: target, flags: promotionType.uint16 or flags)) continue - result.add(Move(startSquare: location, targetSquare: target, flags: flags)) + result.add(Move(startSquare: square, targetSquare: target, flags: flags)) -proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = - ## Generates moves for the sliding piece in the given location - let piece = self.grid[location.row, location.col] +proc generateSlidingMoves(self: ChessBoard, square: Square): seq[Move] = + ## Generates moves for the sliding piece in the given square + let piece = self.grid[square.rank, square.file] assert piece.kind in [Bishop, Rook, Queen], &"generateSlidingMoves called on a {piece.kind}" - var directions: seq[Location] = @[] + var directions: seq[Square] = @[] # Only check in the right directions for the chosen piece if piece.kind in [Bishop, Queen]: @@ -952,9 +1110,9 @@ proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = directions.add(piece.color.bottomSide()) directions.add(piece.color.rightSide()) directions.add(piece.color.leftSide()) - let pinned = self.getPinnedDirections(location) + let pinned = self.getPinnedDirections(square) if pinned.len() > 0: - var newDirections: seq[Location] = @[] + var newDirections: seq[Square] = @[] for direction in directions: if direction in pinned: newDirections.add(direction) @@ -965,14 +1123,14 @@ proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = for direction in directions: # Slide in this direction as long as it's possible var - square: Location = location + square: Square = square otherPiece: Piece while true: square = square + direction # End of board reached if not square.isValid(): break - otherPiece = self.grid[square.row, square.col] + otherPiece = self.grid[square] # A friendly piece is in the way if otherPiece.color == piece.color: break @@ -991,18 +1149,18 @@ proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = # it and stop going any further if otherPiece.kind != King: # Can't capture the king - result.add(Move(startSquare: location, targetSquare: square, flags: Capture.uint16)) + result.add(Move(startSquare: square, targetSquare: square, flags: Capture.uint16)) break # Target square is empty, keep going - result.add(Move(startSquare: location, targetSquare: square)) + result.add(Move(startSquare: square, targetSquare: square)) -proc generateKingMoves(self: ChessBoard, location: Location): seq[Move] = - ## Generates moves for the king in the given location +proc generateKingMoves(self: ChessBoard, square: Square): seq[Move] = + ## Generates moves for the king in the given square var - piece = self.grid[location.row, location.col] + piece = self.grid[square.rank, square.file] assert piece.kind == King, &"generateKingMoves called on a {piece.kind}" - var directions: seq[Location] = @[piece.color.topLeftDiagonal(), + var directions: seq[Square] = @[piece.color.topLeftDiagonal(), piece.color.topRightDiagonal(), piece.color.bottomRightDiagonal(), piece.color.bottomLeftDiagonal(), @@ -1019,7 +1177,7 @@ proc generateKingMoves(self: ChessBoard, location: Location): seq[Move] = var flag = Default for direction in directions: # Step in this direction once - let square: Location = location + direction + let square: Square = square + direction # End of board reached if not square.isValid(): continue @@ -1031,7 +1189,7 @@ proc generateKingMoves(self: ChessBoard, location: Location): seq[Move] = flag = CastleShort else: flag = Default - let otherPiece = self.grid[square.row, square.col] + let otherPiece = self.grid[square] if otherPiece.color == piece.color.opposite(): flag = Capture # A friendly piece is in the way, move onto the next direction @@ -1039,15 +1197,15 @@ proc generateKingMoves(self: ChessBoard, location: Location): seq[Move] = continue # Target square is empty or contains an enemy piece: # All good for us! - result.add(Move(startSquare: location, targetSquare: square, flags: flag.uint16)) + result.add(Move(startSquare: square, targetSquare: square, flags: flag.uint16)) -proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] = - ## Generates moves for the knight in the given location +proc generateKnightMoves(self: ChessBoard, square: Square): seq[Move] = + ## Generates moves for the knight in the given square var - piece = self.grid[location.row, location.col] + piece = self.grid[square.rank, square.file] assert piece.kind == Knight, &"generateKnightMoves called on a {piece.kind}" - var directions: seq[Location] = @[piece.color.bottomLeftKnightMove(), + var directions: seq[Square] = @[piece.color.bottomLeftKnightMove(), piece.color.bottomRightKnightMove(), piece.color.topLeftKnightMove(), piece.color.topRightKnightMove(), @@ -1055,7 +1213,7 @@ proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] = piece.color.bottomRightKnightMove(long=false), piece.color.topLeftKnightMove(long=false), piece.color.topRightKnightMove(long=false)] - let pinned = self.getPinnedDirections(location) + let pinned = self.getPinnedDirections(square) if pinned.len() > 0: # Knight is pinned: can't move! return @[] @@ -1063,11 +1221,11 @@ proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] = let resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color) for direction in directions: # Jump to this square - let square: Location = location + direction + let square: Square = square + direction # End of board reached if not square.isValid(): continue - let otherPiece = self.grid[square.row, square.col] + let otherPiece = self.grid[square] # A friendly piece or the opponent king is is in the way if otherPiece.color == piece.color or otherPiece.kind == King: continue @@ -1076,10 +1234,10 @@ proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] = if otherPiece.color != None: # Target square contains an enemy piece: capture # it - result.add(Move(startSquare: location, targetSquare: square, flags: Capture.uint16)) + result.add(Move(startSquare: square, targetSquare: square, flags: Capture.uint16)) else: # Target square is empty - result.add(Move(startSquare: location, targetSquare: square)) + result.add(Move(startSquare: square, targetSquare: square)) proc checkInsufficientMaterialPieceCount(self: ChessBoard, color: PieceColor): bool = @@ -1139,10 +1297,10 @@ proc checkInsufficientMaterial(self: ChessBoard): bool = return true -proc generateMoves(self: ChessBoard, location: Location): seq[Move] = +proc generateMoves(self: ChessBoard, square: Square): seq[Move] = ## Returns the list of possible legal chess moves for the - ## piece in the given location - if self.position.halfMoveClock == 100: + ## piece in the given square + if self.position.halfMoveClock >= 100: # Draw by 50-move rule return @[] # TODO: Check for draw by insufficient material @@ -1150,19 +1308,20 @@ proc generateMoves(self: ChessBoard, location: Location): seq[Move] = if self.checkInsufficientMaterial(): return @[] ]# - let piece = self.grid[location.row, location.col] + let piece = self.grid[square.rank, square.file] case piece.kind: of Queen, Bishop, Rook: - return self.generateSlidingMoves(location) + return self.generateSlidingMoves(square) of Pawn: - return self.generatePawnMoves(location) + return self.generatePawnMoves(square) of King: - return self.generateKingMoves(location) + return self.generateKingMoves(square) of Knight: - return self.generateKnightMoves(location) + return self.generateKnightMoves(square) else: return @[] + proc generateAllMoves*(self: ChessBoard): seq[Move] = ## Returns the list of all possible legal moves ## in the current position @@ -1173,8 +1332,8 @@ proc generateAllMoves*(self: ChessBoard): seq[Move] = result.add(move) -proc isAttacked*(self: ChessBoard, loc: Location, color: PieceColor = None): bool = - ## Returns whether the given location is attacked +proc isAttacked*(self: ChessBoard, square: Square, color: PieceColor = None): bool = + ## Returns whether the given square is attacked ## by the given color var color = color if color == None: @@ -1182,54 +1341,54 @@ proc isAttacked*(self: ChessBoard, loc: Location, color: PieceColor = None): boo case color: of Black: for attack in self.position.attacked.black: - if attack.target == loc: + if attack.target == square: return true of White: for attack in self.position.attacked.white: - if attack.target == loc: + if attack.target == square: return true of None: discard -proc getAttackers*(self: ChessBoard, loc: Location, color: PieceColor): seq[Location] = +proc getAttackers*(self: ChessBoard, square: Square, color: PieceColor): seq[Square] = ## Returns all the attackers of the given color ## for the given square case color: of Black: for attack in self.position.attacked.black: - if attack.target == loc: + if attack.target == square: result.add(attack.source) of White: for attack in self.position.attacked.white: - if attack.target == loc: + if attack.target == square: result.add(attack.source) of None: discard -proc getAttacks*(self: ChessBoard, loc: Location): Attacked = +proc getAttacks*(self: ChessBoard, square: Square): Attacked = ## Returns all the squares attacked by the piece in the given - ## location - let piece = self.grid[loc.row, loc.col] + ## square + let piece = self.grid[square.rank, square.file] case piece.color: of Black: for attack in self.position.attacked.black: - if attack.source == loc: + if attack.source == square: result.add(attack) of White: for attack in self.position.attacked.white: - if attack.source == loc: + if attack.source == square: result.add(attack) of None: discard -proc getAttackFor*(self: ChessBoard, source, target: Location): tuple[source, target, direction: Location] = +proc getAttackFor*(self: ChessBoard, source, target: Square): tuple[source, target, direction: Square] = ## Returns the first attack from the given source to the ## given target square - result = (emptyLocation(), emptyLocation(), emptyLocation()) - let piece = self.grid[source.row, source.col] + result = (emptySquare(), emptySquare(), emptySquare()) + let piece = self.grid[source] case piece.color: of Black: for attack in self.position.attacked.black: @@ -1246,10 +1405,10 @@ proc getAttackFor*(self: ChessBoard, source, target: Location): tuple[source, ta proc isAttacked*(self: ChessBoard, square: string): bool = ## Returns whether the given square is attacked ## by the current - return self.isAttacked(square.algebraicToLocation()) + return self.isAttacked(square.algebraicToSquare()) -func addAttack(self: ChessBoard, attack: tuple[source, target, direction: Location], color: PieceColor) {.inline.} = +func addAttack(self: ChessBoard, attack: tuple[source, target, direction: Square], color: PieceColor) {.inline.} = if attack.source.isValid() and attack.target.isValid(): case color: of White: @@ -1260,37 +1419,37 @@ func addAttack(self: ChessBoard, attack: tuple[source, target, direction: Locati discard -proc getPinnedDirections(self: ChessBoard, loc: Location): seq[Location] = +proc getPinnedDirections(self: ChessBoard, square: Square): seq[Square] = ## Returns all the directions along which the piece in the given - ## location is pinned. If the result is non-empty, the piece at - ## the given location is only allowed to move along the directions + ## square is pinned. If the result is non-empty, the piece at + ## the given square is only allowed to move along the directions ## returned by this function - let piece = self.grid[loc.row, loc.col] + let piece = self.grid[square.rank, square.file] case piece.color: of None: discard of White: for pin in self.position.pinned.black: - if pin.target == loc: + if pin.target == square: result.add(pin.direction) of Black: for pin in self.position.pinned.white: - if pin.target == loc: + if pin.target == square: result.add(pin.direction) proc updatePawnAttacks(self: ChessBoard) = ## Internal helper of updateAttackedSquares - for loc in self.position.pieces.white.pawns: + for sq in self.position.pieces.white.pawns: # Pawns are special in how they capture (i.e. the # squares they can regularly move to do not match # the squares they can capture on. Sneaky fucks) - self.addAttack((loc, loc + White.topRightDiagonal(), White.topRightDiagonal()), White) - self.addAttack((loc, loc + White.topLeftDiagonal(), White.topLeftDiagonal()), White) + self.addAttack((sq, sq + White.topRightDiagonal(), White.topRightDiagonal()), White) + self.addAttack((sq, sq + White.topLeftDiagonal(), White.topLeftDiagonal()), White) # We do the same thing for black - for loc in self.position.pieces.black.pawns: - self.addAttack((loc, loc + Black.topRightDiagonal(), Black.topRightDiagonal()), Black) - self.addAttack((loc, loc + Black.topLeftDiagonal(), Black.topLeftDiagonal()), Black) + for sq in self.position.pieces.black.pawns: + self.addAttack((sq, sq + Black.topRightDiagonal(), Black.topRightDiagonal()), Black) + self.addAttack((sq, sq + Black.topLeftDiagonal(), Black.topLeftDiagonal()), Black) proc updateKingAttacks(self: ChessBoard) = @@ -1339,11 +1498,11 @@ proc updateKnightAttacks(self: ChessBoard) = self.addAttack((loc, loc + Black.bottomRightKnightMove(long=false), Black.bottomRightKnightMove(long=false)), Black) -proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked, pins: Attacked] = +proc getSlidingAttacks(self: ChessBoard, square: Square): tuple[attacks: Attacked, pins: Attacked] = ## Internal helper of updateSlidingAttacks var - directions: seq[Location] = @[] - let piece = self.grid[loc.row, loc.col] + directions: seq[Square] = @[] + let piece = self.grid[square.rank, square.file] if piece.kind in [Bishop, Queen]: directions.add(piece.color.topLeftDiagonal()) directions.add(piece.color.topRightDiagonal()) @@ -1358,19 +1517,19 @@ proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked for direction in directions: var - square = loc + currentSquare = square otherPiece: Piece # Slide in this direction as long as it's possible while true: - square = square + direction + currentSquare = currentSquare + direction # End of board reached if not square.isValid(): break - otherPiece = self.grid[square.row, square.col] + otherPiece = self.grid[square] # Target square is attacked (even if a friendly piece # is present, because in this case we're defending # it) - result.attacks.add((loc, square, direction)) + result.attacks.add((square, currentSquare, direction)) # Empty square, keep going if otherPiece.color == None: continue @@ -1381,27 +1540,27 @@ proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked # immediately because we first want # to check if we've pinned it to the king var - otherSquare: Location = square + otherSquare: Square = square behindPiece: Piece while true: otherSquare = otherSquare + direction if not otherSquare.isValid(): break - behindPiece = self.grid[otherSquare.row, otherSquare.col] + behindPiece = self.grid[otherSquare] if behindPiece.color == None: continue if behindPiece.color == piece.color.opposite and behindPiece.kind == King: # The enemy king is behind this enemy piece: pin it along # this axis in both directions - result.pins.add((loc, square, direction)) - result.pins.add((loc, square, -direction)) - if otherPiece.kind == Pawn and square.row == otherPiece.getStartRow(): + result.pins.add((square, currentSquare, direction)) + result.pins.add((square, currentSquare, -direction)) + if otherPiece.kind == Pawn and square.rank == otherPiece.getStartRank(): # The pinned piece is a pawn which hasn't moved yet: # we allow it to move two squares as well - if square.col == loc.col: + if square.file == square.file: # The pawn can only push two squares if it's being pinned from the # top side (relative to the pawn itself) - result.pins.add((loc, square, otherPiece.color.doublePush())) + result.pins.add((square, currentSquare, otherPiece.color.doublePush())) else: break else: @@ -1410,7 +1569,7 @@ proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked # valid) let target = square + direction if target.isValid(): - result.attacks.add((loc, target, direction)) + result.attacks.add((square, target, direction)) break @@ -1460,117 +1619,174 @@ proc updateAttackedSquares(self: ChessBoard) = self.updateKingAttacks() -proc removePiece(self: ChessBoard, location: Location, attack: bool = true) = - ## Removes a piece from the board, updating necessary - ## metadata - var piece = self.grid[location.row, location.col] - self.grid[location.row, location.col] = emptyPiece() +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] case piece.color: of White: case piece.kind: of Pawn: - self.position.pieces.white.pawns.delete(self.position.pieces.white.pawns.find(location)) + self.position.bitboards.white.pawns.uint64.clearBit(square.coordToIndex()) of Bishop: - self.position.pieces.white.bishops.delete(self.position.pieces.white.bishops.find(location)) + self.position.bitboards.white.bishops.uint64.clearBit(square.coordToIndex()) of Knight: - self.position.pieces.white.knights.delete(self.position.pieces.white.knights.find(location)) + self.position.bitboards.white.knights.uint64.clearBit(square.coordToIndex()) of Rook: - self.position.pieces.white.rooks.delete(self.position.pieces.white.rooks.find(location)) + self.position.bitboards.white.rooks.uint64.clearBit(square.coordToIndex()) of Queen: - self.position.pieces.white.queens.delete(self.position.pieces.white.queens.find(location)) + self.position.bitboards.white.queens.uint64.clearBit(square.coordToIndex()) of King: - doAssert false, "removePiece: attempted to remove the white king" + self.position.bitboards.white.king.uint64.clearBit(square.coordToIndex()) else: - discard + discard of Black: case piece.kind: of Pawn: - self.position.pieces.black.pawns.delete(self.position.pieces.black.pawns.find(location)) + self.position.bitboards.black.pawns.uint64.clearBit(square.coordToIndex()) of Bishop: - self.position.pieces.black.bishops.delete(self.position.pieces.black.bishops.find(location)) + self.position.bitboards.black.bishops.uint64.clearBit(square.coordToIndex()) of Knight: - self.position.pieces.black.knights.delete(self.position.pieces.black.knights.find(location)) + self.position.bitboards.black.knights.uint64.clearBit(square.coordToIndex()) of Rook: - self.position.pieces.black.rooks.delete(self.position.pieces.black.rooks.find(location)) + self.position.bitboards.black.rooks.uint64.clearBit(square.coordToIndex()) of Queen: - self.position.pieces.black.queens.delete(self.position.pieces.black.queens.find(location)) + self.position.bitboards.black.queens.uint64.clearBit(square.coordToIndex()) of King: - doAssert false, "removePiece: attempted to remove the black king" + self.position.bitboards.black.king.uint64.clearBit(square.coordToIndex()) else: discard else: discard + + +proc addPieceToBitboard(self: ChessBoard, square: Square, piece: Piece) = + ## Adds the given piece at the given square in the chessboard to + ## its respective bitboard + case piece.color: + of White: + case piece.kind: + of Pawn: + self.position.bitboards.white.pawns.uint64.setBit(square.coordToIndex()) + of Bishop: + self.position.bitboards.white.bishops.uint64.setBit(square.coordToIndex()) + of Knight: + self.position.bitboards.white.knights.uint64.setBit(square.coordToIndex()) + of Rook: + self.position.bitboards.white.rooks.uint64.setBit(square.coordToIndex()) + of Queen: + self.position.bitboards.white.queens.uint64.setBit(square.coordToIndex()) + of King: + self.position.bitboards.white.king.uint64.setBit(square.coordToIndex()) + else: + discard + of Black: + case piece.kind: + of Pawn: + self.position.bitboards.black.pawns.uint64.setBit(square.coordToIndex()) + of Bishop: + self.position.bitboards.black.bishops.uint64.setBit(square.coordToIndex()) + of Knight: + self.position.bitboards.black.knights.uint64.setBit(square.coordToIndex()) + of Rook: + self.position.bitboards.black.rooks.uint64.setBit(square.coordToIndex()) + of Queen: + self.position.bitboards.black.queens.uint64.setBit(square.coordToIndex()) + of King: + self.position.bitboards.black.king.uint64.setBit(square.coordToIndex()) + else: + discard + else: + discard + + +proc removePiece(self: ChessBoard, square: Square, attack: bool = true) = + ## Removes a piece from the board, updating necessary + ## metadata + var piece = self.grid[square] + self.grid[square] = emptyPiece() + self.removePieceFromBitboard(square) if attack: self.updateAttackedSquares() +proc updateMoveBitboards(self: ChessBoard, move: Move) = + ## Updates our bitboard representation after a move: note that this + ## does *not* handle captures, en passant, promotions etc. as those + ## are already called by helpers such as removePiece() and spawnPiece() + var bitboard: uint64 + let piece = self.grid[move.startSquare] + # TODO: Should we use our helpers or is it faster to branch only once? + case piece.color: + of White: + case piece.kind: + of Pawn: + self.position.bitboards.white.pawns.uint64.setBit(move.targetSquare.coordToIndex()) + self.position.bitboards.white.pawns.uint64.clearBit(move.startSquare.coordToIndex()) + of Bishop: + self.position.bitboards.white.bishops.uint64.setBit(move.targetSquare.coordToIndex()) + self.position.bitboards.white.bishops.uint64.clearBit(move.startSquare.coordToIndex()) + of Knight: + self.position.bitboards.white.knights.uint64.setBit(move.targetSquare.coordToIndex()) + self.position.bitboards.white.knights.uint64.clearBit(move.startSquare.coordToIndex()) + of Rook: + self.position.bitboards.white.rooks.uint64.setBit(move.targetSquare.coordToIndex()) + self.position.bitboards.white.rooks.uint64.clearBit(move.startSquare.coordToIndex()) + of Queen: + self.position.bitboards.white.queens.uint64.setBit(move.targetSquare.coordToIndex()) + self.position.bitboards.white.queens.uint64.clearBit(move.startSquare.coordToIndex()) + of King: + self.position.bitboards.white.king.uint64.setBit(move.targetSquare.coordToIndex()) + self.position.bitboards.white.king.uint64.clearBit(move.startSquare.coordToIndex()) + else: + discard + of Black: + case piece.kind: + of Pawn: + self.position.bitboards.black.pawns.uint64.setBit(move.targetSquare.coordToIndex()) + self.position.bitboards.black.pawns.uint64.clearBit(move.startSquare.coordToIndex()) + of Bishop: + self.position.bitboards.black.bishops.uint64.setBit(move.targetSquare.coordToIndex()) + self.position.bitboards.black.bishops.uint64.clearBit(move.startSquare.coordToIndex()) + of Knight: + self.position.bitboards.black.knights.uint64.setBit(move.targetSquare.coordToIndex()) + self.position.bitboards.black.knights.uint64.clearBit(move.startSquare.coordToIndex()) + of Rook: + self.position.bitboards.black.rooks.uint64.setBit(move.targetSquare.coordToIndex()) + self.position.bitboards.black.rooks.uint64.clearBit(move.startSquare.coordToIndex()) + of Queen: + self.position.bitboards.black.queens.uint64.setBit(move.targetSquare.coordToIndex()) + self.position.bitboards.black.queens.uint64.clearBit(move.startSquare.coordToIndex()) + of King: + self.position.bitboards.black.king.uint64.setBit(move.targetSquare.coordToIndex()) + self.position.bitboards.black.king.uint64.clearBit(move.startSquare.coordToIndex()) + else: + discard + else: + discard + + proc movePiece(self: ChessBoard, move: Move, attack: bool = true) = ## Internal helper to move a piece. If attack ## is set to false, then this function does ## not update attacked squares metadata, just ## positional info and the grid itself - let piece = self.grid[move.startSquare.row, move.startSquare.col] + let piece = self.grid[move.startSquare] let targetSquare = self.getPiece(move.targetSquare) if targetSquare.color != None: raise newException(AccessViolationDefect, &"attempted to overwrite a piece! {move}") # Update positional metadata - case piece.color: - of White: - case piece.kind: - of Pawn: - # The way things are structured, we don't care about the order - # of this list, so we can add and remove entries as we please - self.position.pieces.white.pawns.delete(self.position.pieces.white.pawns.find(move.startSquare)) - self.position.pieces.white.pawns.add(move.targetSquare) - of Bishop: - self.position.pieces.white.bishops.delete(self.position.pieces.white.bishops.find(move.startSquare)) - self.position.pieces.white.bishops.add(move.targetSquare) - of Knight: - self.position.pieces.white.knights.delete(self.position.pieces.white.knights.find(move.startSquare)) - self.position.pieces.white.knights.add(move.targetSquare) - of Rook: - self.position.pieces.white.rooks.delete(self.position.pieces.white.rooks.find(move.startSquare)) - self.position.pieces.white.rooks.add(move.targetSquare) - of Queen: - self.position.pieces.white.queens.delete(self.position.pieces.white.queens.find(move.startSquare)) - self.position.pieces.white.queens.add(move.targetSquare) - of King: - self.position.pieces.white.king = move.targetSquare - else: - discard - of Black: - case piece.kind: - of Pawn: - self.position.pieces.black.pawns.delete(self.position.pieces.black.pawns.find(move.startSquare)) - self.position.pieces.black.pawns.add(move.targetSquare) - of Bishop: - self.position.pieces.black.bishops.delete(self.position.pieces.black.bishops.find(move.startSquare)) - self.position.pieces.black.bishops.add(move.targetSquare) - of Knight: - self.position.pieces.black.knights.delete(self.position.pieces.black.knights.find(move.startSquare)) - self.position.pieces.black.knights.add(move.targetSquare) - of Rook: - self.position.pieces.black.rooks.delete(self.position.pieces.black.rooks.find(move.startSquare)) - self.position.pieces.black.rooks.add(move.targetSquare) - of Queen: - self.position.pieces.black.queens.delete(self.position.pieces.black.queens.find(move.startSquare)) - self.position.pieces.black.queens.add(move.targetSquare) - of King: - self.position.pieces.black.king = move.targetSquare - else: - discard - else: - discard + self.updateMoveBitboards(move) # Empty out the starting square - self.grid[move.startSquare.row, move.startSquare.col] = emptyPiece() + self.grid[move.startSquare] = emptyPiece() # Actually move the piece on the board - self.grid[move.targetSquare.row, move.targetSquare.col] = piece + self.grid[move.targetSquare] = piece if attack: self.updateAttackedSquares() -proc movePiece(self: ChessBoard, startSquare, targetSquare: Location, attack: bool = true) = - ## Like the other movePiece(), but with two locations +proc movePiece(self: ChessBoard, startSquare, targetSquare: Square, attack: bool = true) = + ## Like the other movePiece(), but with two squares self.movePiece(Move(startSquare: startSquare, targetSquare: targetSquare), attack) @@ -1584,13 +1800,13 @@ proc doMove(self: ChessBoard, move: Move) = self.positions.add(self.position) # Final checks - let piece = self.grid[move.startSquare.row, move.startSquare.col] + let piece = self.grid[move.startSquare] var halfMoveClock = self.position.halfMoveClock fullMoveCount = self.position.fullMoveCount castlingAvailable = self.position.castlingAvailable - enPassantTarget = emptyLocation() + enPassantTarget = emptySquare() # Needed to detect draw by the 50 move rule if piece.kind == Pawn or move.isCapture() or move.isEnPassant(): halfMoveClock = 0 @@ -1606,26 +1822,26 @@ proc doMove(self: ChessBoard, move: Move) = if piece.kind == Rook: case piece.color: of White: - if move.startSquare.row == piece.getStartRow(): - if move.startSquare.col == 0: + if move.startSquare.rank == piece.getStartRank(): + if move.startSquare.file == 0: # Queen side castlingAvailable.white.queen = false - elif move.startSquare.col == 7: + elif move.startSquare.file == 7: # King side castlingAvailable.white.king = false of Black: - if move.startSquare.row == piece.getStartRow(): - if move.startSquare.col == 0: + if move.startSquare.rank == piece.getStartRank(): + if move.startSquare.file == 0: # Queen side castlingAvailable.black.queen = false - elif move.startSquare.col == 7: + elif move.startSquare.file == 7: # King side castlingAvailable.black.king = false else: discard # Has a rook been captured? if move.isCapture(): - let captured = self.grid[move.targetSquare.row, move.targetSquare.col] + let captured = self.grid[move.targetSquare] if captured.kind == Rook: case captured.color: of White: @@ -1665,7 +1881,8 @@ proc doMove(self: ChessBoard, move: Move) = turn: self.getActiveColor().opposite, castlingAvailable: castlingAvailable, pieces: self.position.pieces, - enPassantSquare: enPassantTarget + enPassantSquare: enPassantTarget, + bitboards: self.position.bitboards ) # Update position metadata @@ -1673,19 +1890,19 @@ proc doMove(self: ChessBoard, move: Move) = # Move the rook onto the # correct file when castling var - location: Location - target: Location + square: Square + target: Square flags: uint16 if move.getCastlingType() == CastleShort: - location = piece.color.kingSideRook() + square = piece.color.kingSideRook() target = shortCastleRook() flags = flags or CastleShort.uint16 else: - location = piece.color.queenSideRook() + square = piece.color.queenSideRook() target = longCastleRook() flags = flags or CastleLong.uint16 - let rook = self.grid[location.row, location.col] - let move = Move(startSquare: location, targetSquare: location + target, flags: flags) + let rook = self.grid[square.rank, square.file] + let move = Move(startSquare: square, targetSquare: square + target, flags: flags) self.movePiece(move, attack=false) if move.isEnPassant(): @@ -1714,52 +1931,15 @@ proc doMove(self: ChessBoard, move: Move) = # Unreachable discard self.updateAttackedSquares() - self.updateBoard() -proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) = +proc spawnPiece(self: ChessBoard, square: Square, piece: Piece) = ## Internal helper to "spawn" a given piece at the given - ## location. Note that this will overwrite whatever piece + ## square. Note that this will overwrite whatever piece ## was previously located there: use with caution. Does ## not automatically update the attacked square metadata - ## or other positional information - case piece.color: - of White: - case piece.kind: - of Pawn: - self.position.pieces.white.pawns.add(location) - of Knight: - self.position.pieces.white.knights.add(location) - of Bishop: - self.position.pieces.white.bishops.add(location) - of Rook: - self.position.pieces.white.rooks.add(location) - of Queen: - self.position.pieces.white.queens.add(location) - of King: - doAssert false, "attempted to spawn a white king" - else: - discard - of Black: - case piece.kind: - of Pawn: - self.position.pieces.black.pawns.add(location) - of Knight: - self.position.pieces.black.knights.add(location) - of Bishop: - self.position.pieces.black.bishops.add(location) - of Rook: - self.position.pieces.black.rooks.add(location) - of Queen: - self.position.pieces.black.queens.add(location) - of King: - doAssert false, "attempted to spawn a black king" - else: - discard - else: - # Unreachable - discard - self.grid[location.row, location.col] = piece + self.addPieceToBitboard(square, piece) + self.grid[square] = piece proc updateBoard*(self: ChessBoard) = @@ -1768,28 +1948,28 @@ proc updateBoard*(self: ChessBoard) = ## in the chessboard for i in 0..63: self.grid[i] = emptyPiece() - for loc in self.position.pieces.white.pawns: - self.grid[loc.row, loc.col] = Piece(color: White, kind: Pawn) - for loc in self.position.pieces.black.pawns: - self.grid[loc.row, loc.col] = Piece(color: Black, kind: Pawn) - for loc in self.position.pieces.white.bishops: - self.grid[loc.row, loc.col] = Piece(color: White, kind: Bishop) - for loc in self.position.pieces.black.bishops: - self.grid[loc.row, loc.col] = Piece(color: Black, kind: Bishop) - for loc in self.position.pieces.white.knights: - self.grid[loc.row, loc.col] = Piece(color: White, kind: Knight) - for loc in self.position.pieces.black.knights: - self.grid[loc.row, loc.col] = Piece(color: Black, kind: Knight) - for loc in self.position.pieces.white.rooks: - self.grid[loc.row, loc.col] = Piece(color: White, kind: Rook) - for loc in self.position.pieces.black.rooks: - self.grid[loc.row, loc.col] = Piece(color: Black, kind: Rook) - for loc in self.position.pieces.white.queens: - self.grid[loc.row, loc.col] = Piece(color: White, kind: Queen) - for loc in self.position.pieces.black.queens: - self.grid[loc.row, loc.col] = Piece(color: Black, kind: Queen) - self.grid[self.position.pieces.white.king.row, self.position.pieces.white.king.col] = Piece(color: White, kind: King) - self.grid[self.position.pieces.black.king.row, self.position.pieces.black.king.col] = Piece(color: Black, kind: King) + for sq in self.position.pieces.white.pawns: + self.grid[sq] = Piece(color: White, kind: Pawn) + for sq in self.position.pieces.black.pawns: + self.grid[sq] = Piece(color: Black, kind: Pawn) + for sq in self.position.pieces.white.bishops: + self.grid[sq] = Piece(color: White, kind: Bishop) + for sq in self.position.pieces.black.bishops: + self.grid[sq] = Piece(color: Black, kind: Bishop) + for sq in self.position.pieces.white.knights: + self.grid[sq] = Piece(color: White, kind: Knight) + for sq in self.position.pieces.black.knights: + self.grid[sq] = Piece(color: Black, kind: Knight) + for sq in self.position.pieces.white.rooks: + self.grid[sq] = Piece(color: White, kind: Rook) + for sq in self.position.pieces.black.rooks: + self.grid[sq] = Piece(color: Black, kind: Rook) + for sq in self.position.pieces.white.queens: + self.grid[sq] = Piece(color: White, kind: Queen) + for sq in self.position.pieces.black.queens: + self.grid[sq] = Piece(color: Black, kind: Queen) + self.grid[self.position.pieces.white.king] = Piece(color: White, kind: King) + self.grid[self.position.pieces.black.king] = Piece(color: Black, kind: King) proc undoLastMove*(self: ChessBoard) = @@ -1935,10 +2115,10 @@ proc toFEN*(self: ChessBoard): string = result &= "q" result &= " " # En passant target - if self.getEnPassantTarget() == emptyLocation(): + if self.getEnPassantTarget() == emptySquare(): result &= "-" else: - result &= self.getEnPassantTarget().locationToAlgebraic() + result &= self.getEnPassantTarget().squareToAlgebraic() result &= " " # Halfmove clock result &= $self.getHalfMoveCount() @@ -1955,7 +2135,7 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa if not bulk: if len(moves) == 0 and self.inCheck(): result.checkmates = 1 - # TODO: Should we count stalemates? + # TODO: Should we count stalemates/draws? if ply == 0: result.nodes = 1 return @@ -1974,7 +2154,7 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa postfix = "q" else: postfix = "" - echo &"{move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()}{postfix}: 1" + echo &"{move.startSquare.squareToAlgebraic()}{move.targetSquare.squareToAlgebraic()}{postfix}: 1" if verbose: echo "" return (uint64(len(moves)), 0, 0, 0, 0, 0, 0) @@ -1983,16 +2163,16 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa if verbose: let canCastle = self.canCastle(self.getActiveColor()) echo &"Ply (from root): {self.position.plyFromRoot}" - echo &"Move: {move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()}, from ({move.startSquare.row}, {move.startSquare.col}) to ({move.targetSquare.row}, {move.targetSquare.col})" + echo &"Move: {move.startSquare.squareToAlgebraic()}{move.targetSquare.squareToAlgebraic()}, from ({move.startSquare.rank}, {move.startSquare.file}) to ({move.targetSquare.rank}, {move.targetSquare.file})" echo &"Turn: {self.getActiveColor()}" - echo &"Piece: {self.grid[move.startSquare.row, move.startSquare.col].kind}" + echo &"Piece: {self.grid[move.startSquare].kind}" echo &"Flags: {move.getFlags()}" echo &"In check: {(if self.inCheck(): \"yes\" else: \"no\")}" echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}" echo &"Position before move: {self.toFEN()}" stdout.write("En Passant target: ") - if self.getEnPassantTarget() != emptyLocation(): - echo self.getEnPassantTarget().locationToAlgebraic() + if self.getEnPassantTarget() != emptySquare(): + echo self.getEnPassantTarget().squareToAlgebraic() else: echo "None" echo "\n", self.pretty() @@ -2039,7 +2219,7 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa postfix = "q" else: discard - echo &"{move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()}{postfix}: {next.nodes}" + echo &"{move.startSquare.squareToAlgebraic()}{move.targetSquare.squareToAlgebraic()}{postfix}: {next.nodes}" if verbose: echo "" result.nodes += next.nodes @@ -2112,17 +2292,17 @@ proc handleMoveCommand(board: ChessBoard, command: seq[string]): Move {.discarda echo &"Error: move: invalid move syntax" return var - startSquare: Location - targetSquare: Location + startSquare: Square + targetSquare: Square flags: uint16 try: - startSquare = moveString[0..1].algebraicToLocation() + startSquare = moveString[0..1].algebraicToSquare() except ValueError: echo &"Error: move: invalid start square ({moveString[0..1]})" return try: - targetSquare = moveString[2..3].algebraicToLocation() + targetSquare = moveString[2..3].algebraicToSquare() except ValueError: echo &"Error: move: invalid target square ({moveString[2..3]})" return @@ -2131,10 +2311,10 @@ proc handleMoveCommand(board: ChessBoard, command: seq[string]): Move {.discarda # we have to figure out all the flags by ourselves (whether it's a double # push, a capture, a promotion, castling, etc.) - if board.grid[targetSquare.row, targetSquare.col].kind != Empty: + if board.grid[targetSquare].kind != Empty: flags = flags or Capture.uint16 - elif board.grid[startSquare.row, startSquare.col].kind == Pawn and abs(startSquare.row - targetSquare.row) == 2: + elif board.grid[startSquare].kind == Pawn and abs(startSquare.rank - targetSquare.rank) == 2: flags = flags or DoublePush.uint16 if len(moveString) == 5: @@ -2319,8 +2499,8 @@ proc main: int = echo &"Active color: {board.getActiveColor()}" of "ep": let target = board.getEnPassantTarget() - if target != emptyLocation(): - echo &"En passant target: {target.locationToAlgebraic()}" + if target != emptySquare(): + echo &"En passant target: {target.squareToAlgebraic()}" else: echo "En passant target: None" of "get": @@ -2347,6 +2527,13 @@ proc main: int = return 0 +func createMove(startSquare, targetSquare: string, flags: seq[MoveFlag] = @[]): Move = + result = Move(startSquare: startSquare.algebraicToSquare(), + targetSquare: targetSquare.algebraicToSquare(), flags: Default.uint16) + for flag in flags: + result.flags = result.flags or flag.uint16 + + when isMainModule: @@ -2373,7 +2560,7 @@ when isMainModule: testPieceCount(b, King, Black, 1) - # Ensure pieces are in the correct location + # Ensure pieces are in the correct squares # Pawns for loc in ["a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2"]: @@ -2401,6 +2588,10 @@ when isMainModule: # Queens testPiece(b.getPiece("d1"), Queen, White) testPiece(b.getPiece("d8"), Queen, Black) + echo b.getBitboard(b.getPiece("a2")).toBin() + b.makeMove(createMove("a2", "a4", @[DoublePush])) + echo b.getBitboard(b.getPiece("a4")).toBin() + echo b.getEnPassantTarget() setControlCHook(proc () {.noconv.} = quit(0)) - quit(main()) \ No newline at end of file + # quit(main()) \ No newline at end of file