diff --git a/src/Chess/bitboards.nim b/src/Chess/bitboards.nim new file mode 100644 index 0000000..805e6d9 --- /dev/null +++ b/src/Chess/bitboards.nim @@ -0,0 +1,148 @@ +## Implements low-level bit operations + + +import std/bitops +import std/strutils + + +import pieces + + +type + Bitboard* = distinct uint64 + ## A bitboard + + +# 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): Bitboard = Bitboard(a.uint64 + b.uint64) +func `-`*(a, b: Bitboard): Bitboard = Bitboard(a.uint64 - b.uint64) +func `div`*(a, b: Bitboard): Bitboard = Bitboard(a.uint64 div b.uint64) +func `*`*(a, b: Bitboard): Bitboard = Bitboard(a.uint64 * b.uint64) +func `+`*(a: Bitboard, b: SomeUnsignedInt): Bitboard = Bitboard(a.uint64 + b) +func `-`*(a: Bitboard, b: SomeUnsignedInt): Bitboard = Bitboard(a.uint64 - b.uint64) +func `div`*(a: Bitboard, b: SomeUnsignedInt): Bitboard = Bitboard(a.uint64 div b) +func `*`*(a: Bitboard, b: SomeUnsignedInt): Bitboard = Bitboard(a.uint64 * b) +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)) + +proc bitboardToSquare*(b: Bitboard): Square = Square(b.uint64.countTrailingZeroBits().indexToCoord()) + +func toBin*(x: Bitboard, b: Positive = 64): string = toBin(BiggestInt(x), b) +func toBin*(x: uint64, b: Positive = 64): string = toBin(Bitboard(x), b) + + +iterator items*(self: Bitboard): Square = + ## Iterates ove the given bitboard + ## and returns all the squares that + ## are set + var bits = self + while bits != 0: + yield bits.bitboardToSquare() + bits = bits and bits - 1 + + +iterator pairs*(self: Bitboard): tuple[i: int, sq: Square] = + var i = 0 + for item in self: + yield (i, item) + inc(i) + + +func pretty*(self: Bitboard): string = + + iterator items(self: Bitboard): uint8 = + ## Iterates over all the bits in the + ## given bitboard + for i in 0..63: + yield self.uint64.bitsliced(i..i).uint8 + + + iterator pairs(self: Bitboard): (int, uint8) = + var i = 0 + for bit in self: + yield (i, bit) + inc(i) + + ## Returns a prettyfied version of + ## the given bitboard + for i, bit in self: + if i > 0 and i mod 8 == 0: + result &= "\n" + result &= $bit + + +func computeDiagonalpieces: array[14, Bitboard] {.compileTime.} = + ## Precomputes all the pieces 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 diagonalpieces = computeDiagonalpieces() + + +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 \ No newline at end of file diff --git a/src/Chess/board.nim b/src/Chess/board.nim index f93ca9f..98cfc63 100644 --- a/src/Chess/board.nim +++ b/src/Chess/board.nim @@ -18,75 +18,15 @@ import std/times import std/math import std/bitops - -type - - PieceColor* = enum - ## A piece color enumeration - None = 0'i8, - White, - Black - - PieceKind* = enum - ## A chess piece enumeration - Empty = 0'i8, # No piece - Bishop = 'b', - King = 'k' - Knight = 'n', - Pawn = 'p', - Queen = 'q', - Rook = 'r', - - Direction* = enum - ## A move direction enumeration - Forward, - Backward, - Left, - Right - ForwardLeft, - ForwardRight, - BackwardLeft, - BackwardRight +import bitboards +import pieces +import moves - Piece* = object - ## A chess piece - color*: PieceColor - kind*: PieceKind +type - MoveFlag* = enum - ## An enumeration of move flags - Default = 0'u16, # No flag - EnPassant = 1, # Move is a capture with en passant - Capture = 2, # Move is a capture - DoublePush = 4, # Move is a double pawn push - # Castling metadata - CastleLong = 8, - CastleShort = 16, - # Pawn promotion metadata - PromoteToQueen = 32, - PromoteToRook = 64, - PromoteToBishop = 128, - PromoteToKnight = 256 - - Bitboard* = distinct uint64 - - 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*: Square - targetSquare*: Square - flags*: uint16 - Position* = object ## A chess position # Did the rooks on either side or the king move? @@ -107,9 +47,8 @@ type # Active color turn: PieceColor - - # Bitboards for all pieces - bitboards: tuple[white, black: tuple[king, queens, rooks, bishops, knights, pawns: Bitboard]] + # Positional bitboards for all pieces + pieces: tuple[white, black: tuple[king, queens, rooks, bishops, knights, pawns: Bitboard]] ChessBoard* = ref object @@ -122,22 +61,12 @@ type position: Position # List of all previously reached positions positions: seq[Position] + # Index of the current position + currPos: int # A bunch of simple utility functions and forward declarations - - -func emptyPiece*: Piece {.inline.} = Piece(kind: Empty, color: None) -func emptySquare*: Square {.inline.} = (-1 , -1) -func opposite*(c: PieceColor): PieceColor {.inline.} = (if c == White: Black else: White) -func algebraicToSquare*(s: string): Square {.inline.} proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} -func emptyMove*: Move {.inline.} = Move(startSquare: emptySquare(), targetSquare: emptySquare()) -func `+`*(a, b: Square): Square {.inline.} = (a.rank + b.rank, a.file + b.file) -func `-`*(a: Square): Square {.inline.} = (-a.rank, -a.file) -func `-`*(a, b: Square): Square{.inline.} = (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] @@ -153,7 +82,7 @@ proc spawnPiece(self: ChessBoard, square: Square, piece: Piece) # 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 unmakeMove*(self: ChessBoard) proc movePiece(self: ChessBoard, move: Move, attack: bool = true) proc removePiece(self: ChessBoard, square: Square, attack: bool = true) @@ -165,16 +94,6 @@ proc extend[T](self: var seq[T], other: openarray[T]) {.inline.} = proc updateBoard*(self: ChessBoard) -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 - - -func makeSquare(rank, file: SomeInteger): Square = (rank: rank.int8, file: file.int8) - - # 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): Square {.inline.} = (if color == White: (-1, -1) else: (1, 1)) @@ -246,6 +165,7 @@ func kingSideRook(color: PieceColor): Square {.inline.} = (if color == White: (7 func queenSideRook(color: PieceColor): Square {.inline.} = (if color == White: (7, 0) else: (0, 0)) + # A bunch of getters func getActiveColor*(self: ChessBoard): PieceColor {.inline.} = ## Returns the currently active color @@ -319,14 +239,8 @@ proc newChessboard: ChessBoard = ## Returns a new, empty chessboard new(result) for i in 0..63: - result.grid[i] = emptyPiece() - result.position = Position(enPassantSquare: emptySquare(), turn: White) - - -# 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 div 8).int8, (index mod 8).int8) + result.grid[i] = nullPiece() + result.position = Position(enPassantSquare: nullSquare(), turn: White) # Indexing operations func `[]`(self: array[64, Piece], row, column: Natural): Piece {.inline.} = self[coordToIndex(row, column)] @@ -334,134 +248,6 @@ proc `[]=`(self: var array[64, Piece], row, column: Natural, piece: Piece) {.inl 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): Bitboard = Bitboard(a.uint64 + b.uint64) -func `-`(a, b: Bitboard): Bitboard = Bitboard(a.uint64 - b.uint64) -func `div`(a, b: Bitboard): Bitboard = Bitboard(a.uint64 div b.uint64) -func `*`(a, b: Bitboard): Bitboard = Bitboard(a.uint64 * b.uint64) -func `+`(a: Bitboard, b: SomeUnsignedInt): Bitboard = Bitboard(a.uint64 + b) -func `-`(a: Bitboard, b: SomeUnsignedInt): Bitboard = Bitboard(a.uint64 - b.uint64) -func `div`(a: Bitboard, b: SomeUnsignedInt): Bitboard = Bitboard(a.uint64 div b) -func `*`(a: Bitboard, b: SomeUnsignedInt): Bitboard = Bitboard(a.uint64 * b) -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)) - -proc bitboardToSquare(b: Bitboard): Square = Square(b.uint64.countTrailingZeroBits().indexToCoord()) - -func toBin(x: Bitboard, b: Positive = 64): string = toBin(BiggestInt(x), b) -func toBin(x: uint64, b: Positive = 64): string = toBin(Bitboard(x), b) - - -iterator items(self: Bitboard): Square = - ## Iterates ove the given bitboard - ## and returns all the squares that - ## are set - var bits = self - while bits != 0: - yield bits.bitboardToSquare() - bits = bits and bits - 1 - - -func pretty(self: Bitboard): string = - - iterator items(self: Bitboard): uint8 = - ## Iterates over all the bits in the - ## given bitboard - for i in 0..63: - yield self.uint64.bitsliced(i..i).uint8 - - - iterator pairs(self: Bitboard): (int, uint8) = - var i = 0 - for bit in self: - yield (i, bit) - inc(i) - - ## Returns a prettyfied version of - ## the given bitboard - for i, bit in self: - if i > 0 and i mod 8 == 0: - result &= "\n" - result &= $bit - - - -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 @@ -475,33 +261,33 @@ func getBitboard(self: ChessBoard, kind: PieceKind, color: PieceColor): Bitboard of White: case kind: of Pawn: - return self.position.bitboards.white.pawns + return self.position.pieces.white.pawns of Knight: - return self.position.bitboards.white.knights + return self.position.pieces.white.knights of Bishop: - return self.position.bitboards.white.bishops + return self.position.pieces.white.bishops of Rook: - return self.position.bitboards.white.rooks + return self.position.pieces.white.rooks of Queen: - return self.position.bitboards.white.queens + return self.position.pieces.white.queens of King: - return self.position.bitboards.white.king + return self.position.pieces.white.king else: discard of Black: case kind: of Pawn: - return self.position.bitboards.black.pawns + return self.position.pieces.black.pawns of Knight: - return self.position.bitboards.black.knights + return self.position.pieces.black.knights of Bishop: - return self.position.bitboards.black.bishops + return self.position.pieces.black.bishops of Rook: - return self.position.bitboards.black.rooks + return self.position.pieces.black.rooks of Queen: - return self.position.bitboards.black.queens + return self.position.pieces.black.queens of King: - return self.position.bitboards.black.king + return self.position.pieces.black.king else: discard else: @@ -553,37 +339,37 @@ proc newChessboardFromFEN*(fen: string): ChessBoard = of Black: case piece.kind: of Pawn: - result.position.bitboards.black.pawns.uint64.uint64.setBit(bitIndex) + result.position.pieces.black.pawns.uint64.uint64.setBit(bitIndex) of Bishop: - result.position.bitboards.black.bishops.uint64.setBit(bitIndex) + result.position.pieces.black.bishops.uint64.setBit(bitIndex) of Knight: - result.position.bitboards.black.knights.uint64.setBit(bitIndex) + result.position.pieces.black.knights.uint64.setBit(bitIndex) of Rook: - result.position.bitboards.black.rooks.uint64.setBit(bitIndex) + result.position.pieces.black.rooks.uint64.setBit(bitIndex) of Queen: - result.position.bitboards.black.queens.uint64.setBit(bitIndex) + result.position.pieces.black.queens.uint64.setBit(bitIndex) of King: - if result.position.bitboards.black.king != Bitboard(0'u64): + if result.position.pieces.black.king != Bitboard(0'u64): raise newException(ValueError, "invalid position: exactly one king of each color must be present") - result.position.bitboards.black.king.uint64.setBit(bitIndex) + result.position.pieces.black.king.uint64.setBit(bitIndex) else: discard of White: case piece.kind: of Pawn: - result.position.bitboards.white.pawns.uint64.setBit(bitIndex) + result.position.pieces.white.pawns.uint64.setBit(bitIndex) of Bishop: - result.position.bitboards.white.bishops.uint64.setBit(bitIndex) + result.position.pieces.white.bishops.uint64.setBit(bitIndex) of Knight: - result.position.bitboards.white.knights.uint64.setBit(bitIndex) + result.position.pieces.white.knights.uint64.setBit(bitIndex) of Rook: - result.position.bitboards.white.rooks.uint64.setBit(bitIndex) + result.position.pieces.white.rooks.uint64.setBit(bitIndex) of Queen: - result.position.bitboards.white.queens.uint64.setBit(bitIndex) + result.position.pieces.white.queens.uint64.setBit(bitIndex) of King: - if result.position.bitboards.white.king != 0: + if result.position.pieces.white.king != 0: raise newException(ValueError, "invalid position: exactly one king of each color must be present") - result.position.bitboards.white.king.uint64.setBit(bitIndex) + result.position.pieces.white.king.uint64.setBit(bitIndex) else: discard else: @@ -663,7 +449,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.bitboards.white.king == Bitboard(0) or result.position.bitboards.black.king == Bitboard(0): + if result.position.pieces.white.king == Bitboard(0) or result.position.pieces.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") @@ -682,33 +468,33 @@ proc countPieces*(self: ChessBoard, kind: PieceKind, color: PieceColor): int = of White: case kind: of Pawn: - return self.position.bitboards.white.pawns.uint64.countSetBits() + return self.position.pieces.white.pawns.uint64.countSetBits() of Bishop: - return self.position.bitboards.white.bishops.uint64.countSetBits() + return self.position.pieces.white.bishops.uint64.countSetBits() of Knight: - return self.position.bitboards.white.knights.uint64.countSetBits() + return self.position.pieces.white.knights.uint64.countSetBits() of Rook: - return self.position.bitboards.white.rooks.uint64.countSetBits() + return self.position.pieces.white.rooks.uint64.countSetBits() of Queen: - return self.position.bitboards.white.queens.uint64.countSetBits() + return self.position.pieces.white.queens.uint64.countSetBits() of King: - return self.position.bitboards.white.king.uint64.countSetBits() + return self.position.pieces.white.king.uint64.countSetBits() else: raise newException(ValueError, "invalid piece type") of Black: case kind: of Pawn: - return self.position.bitboards.black.pawns.uint64.countSetBits() + return self.position.pieces.black.pawns.uint64.countSetBits() of Bishop: - return self.position.bitboards.black.bishops.uint64.countSetBits() + return self.position.pieces.black.bishops.uint64.countSetBits() of Knight: - return self.position.bitboards.black.knights.uint64.countSetBits() + return self.position.pieces.black.knights.uint64.countSetBits() of Rook: - return self.position.bitboards.black.rooks.uint64.countSetBits() + return self.position.pieces.black.rooks.uint64.countSetBits() of Queen: - return self.position.bitboards.black.queens.uint64.countSetBits() + return self.position.pieces.black.queens.uint64.countSetBits() of King: - return self.position.bitboards.black.king.uint64.countSetBits() + return self.position.pieces.black.king.uint64.countSetBits() else: raise newException(ValueError, "invalid piece type") of None: @@ -721,48 +507,6 @@ func countPieces*(self: ChessBoard, piece: Piece): int {.inline.} = return self.countPieces(piece.kind, piece.color) -func rankToColumn(rank: int): int8 {.inline.} = - ## Converts a chess rank (1-indexed) - ## into a 0-indexed column value for our - ## board. This converter is necessary because - ## chess positions are indexed differently with - ## respect to our internal representation - const indeces: array[8, int8] = [7, 6, 5, 4, 3, 2, 1, 0] - return indeces[rank - 1] - - -func rowToFile(row: int): int8 {.inline.} = - ## Converts a row into our grid into - ## a chess file - const indeces: array[8, int8] = [8, 7, 6, 5, 4, 3, 2, 1] - return indeces[row] - - -func 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: - raise newException(ValueError, "algebraic position must be of length 2") - - var s = s.toLowerAscii() - if s[0] notin 'a'..'h': - raise newException(ValueError, &"algebraic position has invalid first character ('{s[0]}')") - if s[1] notin '1'..'8': - raise newException(ValueError, &"algebraic position has invalid second character ('{s[1]}')") - - let rank = int8(uint8(s[0]) - uint8('a')) - # Convert the file character to a number - let file = rankToColumn(int8(uint8(s[1]) - uint8('0'))) - return (file, rank) - - -func squareToAlgebraic*(square: Square): string {.inline.} = - ## Converts a square from our internal rank/file - ## notation to a square in algebraic notation - return &"{char(uint8(square.file) + uint8('a'))}{rowToFile(square.rank)}" - - func getPiece*(self: ChessBoard, square: Square): Piece {.inline.} = ## Gets the piece at the given square return self.grid[square.rank, square.file] @@ -845,9 +589,9 @@ func getKing(self: ChessBoard, color: PieceColor = None): Square {.inline.} = color = self.getActiveColor() case color: of White: - return self.position.bitboards.white.king.uint64.countTrailingZeroBits().indexToCoord() + return self.position.pieces.white.king.uint64.countTrailingZeroBits().indexToCoord() of Black: - return self.position.bitboards.black.king.uint64.countTrailingZeroBits().indexToCoord() + return self.position.pieces.black.king.uint64.countTrailingZeroBits().indexToCoord() else: discard @@ -931,11 +675,11 @@ proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: # is temporarily prohibited on that side case color: of White: - square = self.position.bitboards.white.king.bitboardToSquare() + square = self.position.pieces.white.king.bitboardToSquare() queenSide = color.leftSide() kingSide = color.rightSide() of Black: - square = self.position.bitboards.black.king.bitboardToSquare() + square = self.position.pieces.black.king.bitboardToSquare() queenSide = color.rightSide() kingSide = color.leftSide() of None: @@ -1331,12 +1075,12 @@ proc checkInsufficientMaterial(self: ChessBoard): bool = var darkSquare = 0 lightSquare = 0 - for bishop in self.position.bitboards.black.bishops: + for bishop in self.position.pieces.black.bishops: if bishop.isLightSquare(): lightSquare += 1 else: darkSquare += 1 - for bishop in self.position.bitboards.white.bishops: + for bishop in self.position.pieces.white.bishops: if bishop.isLightSquare(): lightSquare += 1 else: @@ -1436,7 +1180,7 @@ proc getAttacks*(self: ChessBoard, square: Square): Attacked = 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 = (emptySquare(), emptySquare(), emptySquare()) + result = (nullSquare(), nullSquare(), nullSquare()) let piece = self.grid[source] case piece.color: of Black: @@ -1676,33 +1420,33 @@ proc removePieceFromBitboard(self: ChessBoard, square: Square) = of White: case piece.kind: of Pawn: - self.position.bitboards.white.pawns.uint64.clearBit(square.coordToIndex()) + self.position.pieces.white.pawns.uint64.clearBit(square.coordToIndex()) of Bishop: - self.position.bitboards.white.bishops.uint64.clearBit(square.coordToIndex()) + self.position.pieces.white.bishops.uint64.clearBit(square.coordToIndex()) of Knight: - self.position.bitboards.white.knights.uint64.clearBit(square.coordToIndex()) + self.position.pieces.white.knights.uint64.clearBit(square.coordToIndex()) of Rook: - self.position.bitboards.white.rooks.uint64.clearBit(square.coordToIndex()) + self.position.pieces.white.rooks.uint64.clearBit(square.coordToIndex()) of Queen: - self.position.bitboards.white.queens.uint64.clearBit(square.coordToIndex()) + self.position.pieces.white.queens.uint64.clearBit(square.coordToIndex()) of King: - self.position.bitboards.white.king.uint64.clearBit(square.coordToIndex()) + self.position.pieces.white.king.uint64.clearBit(square.coordToIndex()) else: discard of Black: case piece.kind: of Pawn: - self.position.bitboards.black.pawns.uint64.clearBit(square.coordToIndex()) + self.position.pieces.black.pawns.uint64.clearBit(square.coordToIndex()) of Bishop: - self.position.bitboards.black.bishops.uint64.clearBit(square.coordToIndex()) + self.position.pieces.black.bishops.uint64.clearBit(square.coordToIndex()) of Knight: - self.position.bitboards.black.knights.uint64.clearBit(square.coordToIndex()) + self.position.pieces.black.knights.uint64.clearBit(square.coordToIndex()) of Rook: - self.position.bitboards.black.rooks.uint64.clearBit(square.coordToIndex()) + self.position.pieces.black.rooks.uint64.clearBit(square.coordToIndex()) of Queen: - self.position.bitboards.black.queens.uint64.clearBit(square.coordToIndex()) + self.position.pieces.black.queens.uint64.clearBit(square.coordToIndex()) of King: - self.position.bitboards.black.king.uint64.clearBit(square.coordToIndex()) + self.position.pieces.black.king.uint64.clearBit(square.coordToIndex()) else: discard else: @@ -1716,33 +1460,33 @@ proc addPieceToBitboard(self: ChessBoard, square: Square, piece: Piece) = of White: case piece.kind: of Pawn: - self.position.bitboards.white.pawns.uint64.setBit(square.coordToIndex()) + self.position.pieces.white.pawns.uint64.setBit(square.coordToIndex()) of Bishop: - self.position.bitboards.white.bishops.uint64.setBit(square.coordToIndex()) + self.position.pieces.white.bishops.uint64.setBit(square.coordToIndex()) of Knight: - self.position.bitboards.white.knights.uint64.setBit(square.coordToIndex()) + self.position.pieces.white.knights.uint64.setBit(square.coordToIndex()) of Rook: - self.position.bitboards.white.rooks.uint64.setBit(square.coordToIndex()) + self.position.pieces.white.rooks.uint64.setBit(square.coordToIndex()) of Queen: - self.position.bitboards.white.queens.uint64.setBit(square.coordToIndex()) + self.position.pieces.white.queens.uint64.setBit(square.coordToIndex()) of King: - self.position.bitboards.white.king.uint64.setBit(square.coordToIndex()) + self.position.pieces.white.king.uint64.setBit(square.coordToIndex()) else: discard of Black: case piece.kind: of Pawn: - self.position.bitboards.black.pawns.uint64.setBit(square.coordToIndex()) + self.position.pieces.black.pawns.uint64.setBit(square.coordToIndex()) of Bishop: - self.position.bitboards.black.bishops.uint64.setBit(square.coordToIndex()) + self.position.pieces.black.bishops.uint64.setBit(square.coordToIndex()) of Knight: - self.position.bitboards.black.knights.uint64.setBit(square.coordToIndex()) + self.position.pieces.black.knights.uint64.setBit(square.coordToIndex()) of Rook: - self.position.bitboards.black.rooks.uint64.setBit(square.coordToIndex()) + self.position.pieces.black.rooks.uint64.setBit(square.coordToIndex()) of Queen: - self.position.bitboards.black.queens.uint64.setBit(square.coordToIndex()) + self.position.pieces.black.queens.uint64.setBit(square.coordToIndex()) of King: - self.position.bitboards.black.king.uint64.setBit(square.coordToIndex()) + self.position.pieces.black.king.uint64.setBit(square.coordToIndex()) else: discard else: @@ -1753,13 +1497,13 @@ 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.grid[square] = nullPiece() self.removePieceFromBitboard(square) #[if attack: self.updateAttackedSquares()]# -proc updateMoveBitboards(self: ChessBoard, move: Move) = +proc updateMovepieces(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() @@ -1770,45 +1514,45 @@ proc updateMoveBitboards(self: ChessBoard, move: Move) = 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()) + self.position.pieces.white.pawns.uint64.setBit(move.targetSquare.coordToIndex()) + self.position.pieces.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()) + self.position.pieces.white.bishops.uint64.setBit(move.targetSquare.coordToIndex()) + self.position.pieces.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()) + self.position.pieces.white.knights.uint64.setBit(move.targetSquare.coordToIndex()) + self.position.pieces.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()) + self.position.pieces.white.rooks.uint64.setBit(move.targetSquare.coordToIndex()) + self.position.pieces.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()) + self.position.pieces.white.queens.uint64.setBit(move.targetSquare.coordToIndex()) + self.position.pieces.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()) + self.position.pieces.white.king.uint64.setBit(move.targetSquare.coordToIndex()) + self.position.pieces.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()) + self.position.pieces.black.pawns.uint64.setBit(move.targetSquare.coordToIndex()) + self.position.pieces.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()) + self.position.pieces.black.bishops.uint64.setBit(move.targetSquare.coordToIndex()) + self.position.pieces.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()) + self.position.pieces.black.knights.uint64.setBit(move.targetSquare.coordToIndex()) + self.position.pieces.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()) + self.position.pieces.black.rooks.uint64.setBit(move.targetSquare.coordToIndex()) + self.position.pieces.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()) + self.position.pieces.black.queens.uint64.setBit(move.targetSquare.coordToIndex()) + self.position.pieces.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()) + self.position.pieces.black.king.uint64.setBit(move.targetSquare.coordToIndex()) + self.position.pieces.black.king.uint64.clearBit(move.startSquare.coordToIndex()) else: discard else: @@ -1825,9 +1569,9 @@ proc movePiece(self: ChessBoard, move: Move, attack: bool = true) = if targetSquare.color != None: raise newException(AccessViolationDefect, &"attempted to overwrite a piece! {move}") # Update positional metadata - self.updateMoveBitboards(move) + self.updateMovepieces(move) # Empty out the starting square - self.grid[move.startSquare] = emptyPiece() + self.grid[move.startSquare] = nullPiece() # Actually move the piece on the board self.grid[move.targetSquare] = piece #[if attack: @@ -1855,7 +1599,7 @@ proc doMove(self: ChessBoard, move: Move) = halfMoveClock = self.position.halfMoveClock fullMoveCount = self.position.fullMoveCount castlingAvailable = self.position.castlingAvailable - enPassantTarget = emptySquare() + enPassantTarget = nullSquare() # Needed to detect draw by the 50 move rule if piece.kind == Pawn or move.isCapture() or move.isEnPassant(): halfMoveClock = 0 @@ -1930,7 +1674,7 @@ proc doMove(self: ChessBoard, move: Move) = turn: self.getActiveColor().opposite, castlingAvailable: castlingAvailable, enPassantSquare: enPassantTarget, - bitboards: self.position.bitboards + pieces: self.position.pieces ) # Update position metadata @@ -1995,34 +1739,47 @@ proc updateBoard*(self: ChessBoard) = ## according to the positional data stored ## in the chessboard for i in 0..63: - self.grid[i] = emptyPiece() - for sq in self.position.bitboards.white.pawns: + self.grid[i] = nullPiece() + for sq in self.position.pieces.white.pawns: self.grid[sq] = Piece(color: White, kind: Pawn) - for sq in self.position.bitboards.black.pawns: + for sq in self.position.pieces.black.pawns: self.grid[sq] = Piece(color: Black, kind: Pawn) - for sq in self.position.bitboards.white.bishops: + for sq in self.position.pieces.white.bishops: self.grid[sq] = Piece(color: White, kind: Bishop) - for sq in self.position.bitboards.black.bishops: + for sq in self.position.pieces.black.bishops: self.grid[sq] = Piece(color: Black, kind: Bishop) - for sq in self.position.bitboards.white.knights: + for sq in self.position.pieces.white.knights: self.grid[sq] = Piece(color: White, kind: Knight) - for sq in self.position.bitboards.black.knights: + for sq in self.position.pieces.black.knights: self.grid[sq] = Piece(color: Black, kind: Knight) - for sq in self.position.bitboards.white.rooks: + for sq in self.position.pieces.white.rooks: self.grid[sq] = Piece(color: White, kind: Rook) - for sq in self.position.bitboards.black.rooks: + for sq in self.position.pieces.black.rooks: self.grid[sq] = Piece(color: Black, kind: Rook) - for sq in self.position.bitboards.white.queens: + for sq in self.position.pieces.white.queens: self.grid[sq] = Piece(color: White, kind: Queen) - for sq in self.position.bitboards.black.queens: + for sq in self.position.pieces.black.queens: self.grid[sq] = Piece(color: Black, kind: Queen) - self.grid[self.position.bitboards.white.king.bitboardToSquare()] = Piece(color: White, kind: King) - self.grid[self.position.bitboards.black.king.bitboardToSquare()] = Piece(color: Black, kind: King) + self.grid[self.position.pieces.white.king.bitboardToSquare()] = Piece(color: White, kind: King) + self.grid[self.position.pieces.black.king.bitboardToSquare()] = Piece(color: Black, kind: King) -proc undoLastMove*(self: ChessBoard) = - if self.positions.len() > 0: - self.position = self.positions.pop() +proc unmakeMove*(self: ChessBoard) = + ## Reverts to the previous board position, + ## if one exists + if self.currPos > 0: + dec(self.currPos) + self.position = self.positions[self.currPos] + self.updateBoard() + + +proc redoMove*(self: ChessBoard) = + ## Reverts to the next board position, if one + ## exists. Only makes sense after a call to + ## unmakeMove + if self.positions.high() > self.currPos: + inc(self.currPos) + self.position = self.positions[self.currPos] self.updateBoard() @@ -2035,7 +1792,7 @@ proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} = ## Makes a move on the board result = move if not self.isLegal(move): - return emptyMove() + return nullMove() self.doMove(move) @@ -2055,7 +1812,7 @@ proc `$`*(self: ChessBoard): string = result &= "x " continue result &= &"{piece.toChar()} " - result &= &"{rankToColumn(i + 1) + 1}" + result &= &"{fileToColumn(i + 1) + 1}" result &= "\n- - - - - - - -" result &= "\na b c d e f g h" @@ -2116,7 +1873,7 @@ proc pretty*(self: ChessBoard): string = result &= " \x1b[0m" else: result &= &"{piece.toPretty()} \x1b[0m" - result &= &" \x1b[33;1m{rankToColumn(i + 1) + 1}\x1b[0m" + result &= &" \x1b[33;1m{fileToColumn(i + 1) + 1}\x1b[0m" result &= "\n\x1b[31;1ma b c d e f g h" result &= "\x1b[0m" @@ -2163,7 +1920,7 @@ proc toFEN*(self: ChessBoard): string = result &= "q" result &= " " # En passant target - if self.getEnPassantTarget() == emptySquare(): + if self.getEnPassantTarget() == nullSquare(): result &= "-" else: result &= self.getEnPassantTarget().squareToAlgebraic() @@ -2219,7 +1976,7 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa 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() != emptySquare(): + if self.getEnPassantTarget() != nullSquare(): echo self.getEnPassantTarget().squareToAlgebraic() else: echo "None" @@ -2252,7 +2009,7 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa except EOFError: discard let next = self.perft(ply - 1, verbose, bulk=bulk) - self.undoLastMove() + self.unmakeMove() if divide and (not bulk or ply > 1): var postfix = "" if move.isPromotion(): @@ -2342,7 +2099,7 @@ proc handleMoveCommand(board: ChessBoard, command: seq[string]): Move {.discarda var startSquare: Square targetSquare: Square - flags: uint16 + flags: seq[MoveFlag] try: startSquare = moveString[0..1].algebraicToSquare() @@ -2360,28 +2117,28 @@ proc handleMoveCommand(board: ChessBoard, command: seq[string]): Move {.discarda # push, a capture, a promotion, castling, etc.) if board.grid[targetSquare].kind != Empty: - flags = flags or Capture.uint16 + flags.add(Capture) elif board.grid[startSquare].kind == Pawn and abs(startSquare.rank - targetSquare.rank) == 2: - flags = flags or DoublePush.uint16 + flags.add(DoublePush) if len(moveString) == 5: # Promotion case moveString[4]: of 'b': - flags = flags or PromoteToBishop.uint16 + flags.add(PromoteToBishop) of 'n': - flags = flags or PromoteToKnight.uint16 + flags.add(PromoteToKnight) of 'q': - flags = flags or PromoteToQueen.uint16 + flags.add(PromoteToQueen) of 'r': - flags = flags or PromoteToRook.uint16 + flags.add(PromoteToRook) else: echo &"Error: move: invalid promotion type" return - var move = Move(startSquare: startSquare, targetSquare: targetSquare, flags: flags) + var move = createMove(startSquare, targetSquare, flags) if board.getPiece(move.startSquare).kind == King and move.startSquare == board.getActiveColor().getKingStartingSquare(): if move.targetSquare == move.startSquare + longCastleKing(): move.flags = move.flags or CastleLong.uint16 @@ -2390,7 +2147,7 @@ proc handleMoveCommand(board: ChessBoard, command: seq[string]): Move {.discarda if move.targetSquare == board.getEnPassantTarget(): move.flags = move.flags or EnPassant.uint16 result = board.makeMove(move) - if result == emptyMove(): + if result == nullMove(): echo &"Error: move: {moveString} is illegal" @@ -2413,7 +2170,7 @@ proc handlePositionCommand(board: var ChessBoard, command: seq[string]) = of "moves": var j = i + 1 while j < args.len(): - if handleMoveCommand(tempBoard, @["move", args[j]]) == emptyMove(): + if handleMoveCommand(tempBoard, @["move", args[j]]) == nullMove(): return inc(j) inc(i) @@ -2446,7 +2203,7 @@ proc handlePositionCommand(board: var ChessBoard, command: seq[string]) = of "moves": var j = i + 1 while j < args.len(): - if handleMoveCommand(tempBoard, @["move", args[j]]) == emptyMove(): + if handleMoveCommand(tempBoard, @["move", args[j]]) == nullMove(): return inc(j) inc(i) @@ -2542,12 +2299,12 @@ proc main: int = of "pretty", "print", "fen": handlePositionCommand(board, @["position", cmd[0]]) of "undo", "u": - board.undoLastMove() + board.unmakeMove() of "turn": echo &"Active color: {board.getActiveColor()}" of "ep": let target = board.getEnPassantTarget() - if target != emptySquare(): + if target != nullSquare(): echo &"En passant target: {target.squareToAlgebraic()}" else: echo "En passant target: None" @@ -2584,6 +2341,15 @@ when isMainModule: proc testPieceCount(board: ChessBoard, kind: PieceKind, color: PieceColor, count: int) = let pieces = board.countPieces(kind, color) doAssert pieces == count, &"expected {count} pieces of kind {kind} and color {color}, got {pieces} instead" + + proc testPieceBitboard(bitboard: Bitboard, squares: seq[Square]) = + var i = 0 + for square in bitboard: + doAssert squares[i] == square, &"squares[{i}] != bitboard[i]: {squares[i]} != {square}" + inc(i) + if i != squares.len(): + doAssert false, &"bitboard.len() ({i}) != squares.len() ({squares.len()})" + var b = newDefaultChessboard() # Ensure correct number of pieces @@ -2601,7 +2367,8 @@ when isMainModule: testPieceCount(b, King, Black, 1) - # Ensure pieces are in the correct squares + # Ensure pieces are in the correct squares. This is testing the FEN + # parser # Pawns for loc in ["a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2"]: @@ -2630,14 +2397,44 @@ when isMainModule: testPiece(b.getPiece("d1"), Queen, White) testPiece(b.getPiece("d8"), Queen, Black) - b.makeMove(createMove("a2", "a4", @[DoublePush])) - let whitePawns = b.getBitboard(Pawn, White) - echo whitePawns.pretty() - for square in whitePawns: - echo square - doAssert b.getEnPassantTarget() == makeSquare(5, 0) - - - #[setControlCHook(proc () {.noconv.} = quit(0)) + # Ensure our bitboards match with the board + let + whitePawns = b.getBitboard(Pawn, White) + whiteKnights = b.getBitboard(Knight, White) + whiteBishops = b.getBitboard(Bishop, White) + whiteRooks = b.getBitboard(Rook, White) + whiteQueens = b.getBitboard(Queen, White) + whiteKing = b.getBitboard(King, White) + blackPawns = b.getBitboard(Pawn, Black) + blackKnights = b.getBitboard(Knight, Black) + blackBishops = b.getBitboard(Bishop, Black) + blackRooks = b.getBitboard(Rook, Black) + blackQueens = b.getBitboard(Queen, Black) + blackKing = b.getBitboard(King, Black) + whitePawnSquares = @[(6'i8, 0'i8), (6, 1), (6, 2), (6, 3), (6, 4), (6, 5), (6, 6), (6, 7)] + whiteKnightSquares = @[(7'i8, 1'i8), (7, 6)] + whiteBishopSquares = @[(7'i8, 2'i8), (7, 5)] + whiteRookSquares = @[(7'i8, 0'i8), (7, 7)] + whiteQueenSquares = @[(7'i8, 3'i8)] + whiteKingSquares = @[(7'i8, 4'i8)] + blackPawnSquares = @[(1'i8, 0'i8), (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7)] + blackKnightSquares = @[(0'i8, 1'i8), (0, 6)] + blackBishopSquares = @[(0'i8, 2'i8), (0, 5)] + blackRookSquares = @[(0'i8, 0'i8), (0, 7)] + blackQueenSquares = @[(0'i8, 3'i8)] + blackKingSquares = @[(0'i8, 4'i8)] - quit(main())]# \ No newline at end of file + testPieceBitboard(whitePawns, whitePawnSquares) + testPieceBitboard(whiteKnights, whiteKnightSquares) + testPieceBitboard(whiteBishops, whiteBishopSquares) + testPieceBitboard(whiteRooks, whiteRookSquares) + testPieceBitboard(whiteQueens, whiteQueenSquares) + testPieceBitboard(whiteKing, whiteKingSquares) + testPieceBitboard(blackPawns, blackPawnSquares) + testPieceBitboard(blackKnights, blackKnightSquares) + testPieceBitboard(blackBishops, blackBishopSquares) + testPieceBitboard(blackRooks, blackRookSquares) + testPieceBitboard(blackQueens, blackQueenSquares) + testPieceBitboard(blackKing, blackKingSquares) + # setControlCHook(proc () {.noconv.} = quit(0)) + # quit(main()) \ No newline at end of file diff --git a/src/Chess/moves.nim b/src/Chess/moves.nim new file mode 100644 index 0000000..bca16d4 --- /dev/null +++ b/src/Chess/moves.nim @@ -0,0 +1,41 @@ +## Handling of moves +import pieces + + +type + MoveFlag* = enum + ## An enumeration of move flags + Default = 0'u16, # No flag + EnPassant = 1, # Move is a capture with en passant + Capture = 2, # Move is a capture + DoublePush = 4, # Move is a double pawn push + # Castling metadata + CastleLong = 8, + CastleShort = 16, + # Pawn promotion metadata + PromoteToQueen = 32, + PromoteToRook = 64, + PromoteToBishop = 128, + PromoteToKnight = 256 + + Move* = object + ## A chess move + startSquare*: Square + targetSquare*: Square + flags*: uint16 + + +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 + + +func createMove*(startSquare, targetSquare: Square, flags: seq[MoveFlag] = @[]): Move = + result = Move(startSquare: startSquare, targetSquare: targetSquare, flags: Default.uint16) + for flag in flags: + result.flags = result.flags or flag.uint16 + + +func nullMove*: Move {.inline.} = createMove(nullSquare(), nullSquare()) \ No newline at end of file diff --git a/src/Chess/pieces.nim b/src/Chess/pieces.nim new file mode 100644 index 0000000..575514b --- /dev/null +++ b/src/Chess/pieces.nim @@ -0,0 +1,96 @@ +## Low-level handling of squares, board indeces and pieces +import std/strutils +import std/strformat + + +type + Square* = tuple[rank, file: int8] + ## A square + + PieceColor* = enum + ## A piece color enumeration + None = 0'i8, + White, + Black + + PieceKind* = enum + ## A chess piece enumeration + Empty = 0'i8, # No piece + Bishop = 'b', + King = 'k' + Knight = 'n', + Pawn = 'p', + 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 + kind*: PieceKind + + +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 div 8).int8, (index mod 8).int8) +func nullPiece*: Piece {.inline.} = Piece(kind: Empty, color: None) +func nullSquare*: Square {.inline.} = (-1 , -1) +func opposite*(c: PieceColor): PieceColor {.inline.} = (if c == White: Black else: White) +func `+`*(a, b: Square): Square {.inline.} = (a.rank + b.rank, a.file + b.file) +func `-`*(a: Square): Square {.inline.} = (-a.rank, -a.file) +func `-`*(a, b: Square): Square{.inline.} = (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 +func makeSquare*(rank, file: SomeInteger): Square = (rank: rank.int8, file: file.int8) + + +func fileToColumn*(file: int): int8 {.inline.} = + ## Converts a chess file (1-indexed) + ## into a 0-indexed column value for our + ## board. This converter is necessary because + ## chess positions are indexed differently with + ## respect to our internal representation + const indeces: array[8, int8] = [7, 6, 5, 4, 3, 2, 1, 0] + return indeces[file - 1] + + +func rowToRank*(row: int): int8 {.inline.} = + ## Converts a row into our grid into + ## a chess rank + const indeces: array[8, int8] = [8, 7, 6, 5, 4, 3, 2, 1] + return indeces[row] + + +func 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: + raise newException(ValueError, "algebraic position must be of length 2") + + var s = s.toLowerAscii() + if s[0] notin 'a'..'h': + raise newException(ValueError, &"algebraic position has invalid first character ('{s[0]}')") + if s[1] notin '1'..'8': + raise newException(ValueError, &"algebraic position has invalid second character ('{s[1]}')") + + let rank = int8(uint8(s[0]) - uint8('a')) + # Convert the file character to a number + let file = fileToColumn(int8(uint8(s[1]) - uint8('0'))) + return (file, rank) + + +func squareToAlgebraic*(square: Square): string {.inline.} = + ## Converts a square from our internal rank/file + ## notation to a square in algebraic notation + return &"{rowToRank(square.rank)}{char(uint8(square.file) + uint8('a'))}" \ No newline at end of file