diff --git a/src/Chess/board.nim b/src/Chess/board.nim index db61f9c..4c1e857 100644 --- a/src/Chess/board.nim +++ b/src/Chess/board.nim @@ -19,16 +19,7 @@ import std/strutils import std/strformat import std/sequtils - type - # Useful type aliases - Location* = tuple[row, col: int8] - - Attacked = seq[tuple[source, dest: Location]] - - Pieces = tuple[king: Location, queens: seq[Location], rooks: seq[Location], - bishops: seq[Location], knights: seq[Location], - pawns: seq[Location]] PieceColor* = enum ## A piece color enumeration @@ -38,7 +29,7 @@ type PieceKind* = enum ## A chess piece enumeration - Empty = '\0', # No piece + Empty = 0'i8, # No piece Bishop = 'b', King = 'k' Knight = 'n', @@ -54,19 +45,30 @@ type MoveFlag* = enum ## An enumeration of move flags Default = 0'i8, # No flag - EnPassant, # Move is a capture with en passant - Capture, # Move is a capture + EnPassant, # Move is a capture with en passant + Capture, # Move is a capture DoublePush, # Move is a double pawn push # Castling metadata CastleLong, CastleShort, - XRay, # Move is an X-ray attack # Pawn promotion metadata PromoteToQueen, PromoteToRook, PromoteToBishop, PromoteToKnight + # Useful type aliases + Location* = tuple[row, col: int8] + + Attacked = seq[tuple[source, target: Location]] + Pinned = seq[tuple[source, target, direction: Location]] + + Pieces = tuple[king: Location, queens: seq[Location], rooks: seq[Location], + bishops: seq[Location], knights: seq[Location], + pawns: seq[Location]] + + CountData = tuple[nodes: uint64, captures: uint64, castles: uint64, checks: uint64, promotions: uint64, enPassant: uint64, checkmates: uint64] + Move* = object ## A chess move piece*: Piece @@ -91,13 +93,12 @@ type # every 2 ply fullMoveCount: int16 # En passant target square (see https://en.wikipedia.org/wiki/En_passant) - # If en passant is not possible, both the row and - # column of the position will be set to -1 enPassantSquare*: Move # Locations of all pieces pieces: tuple[white: Pieces, black: Pieces] # Potential attacking moves for black and white attacked: tuple[white: Attacked, black: Attacked] + pinned: tuple[white: Pinned, black: Pinned] # Has any piece been captured to reach this position? captured: Piece # Active color @@ -127,6 +128,8 @@ proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} proc makeMove*(self: ChessBoard, startSquare, targetSquare: Location): Move {.discardable.} func emptyMove*: Move {.inline.} = Move(startSquare: emptyLocation(), targetSquare: emptyLocation(), piece: emptyPiece()) 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 proc generateMoves(self: ChessBoard, location: Location): seq[Move] proc isAttacked*(self: ChessBoard, loc: Location, color: PieceColor = None): bool @@ -136,6 +139,13 @@ proc doMove(self: ChessBoard, move: Move) proc pretty*(self: ChessBoard): string proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) proc updateAttackedSquares(self: ChessBoard) +proc getPinnedDirections(self: ChessBoard, loc: Location): seq[Location] + + +proc extend[T](self: var seq[T], other: openarray[T]) {.inline.} = + for x in other: + self.add(x) + # Due to our board layout, directions of movement are reversed for white/black so # we need these helpers to avoid going mad with integer tuples and minus signs @@ -146,7 +156,7 @@ func bottomLeftDiagonal(color: PieceColor): Location {.inline.} = (if color == W 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.} = (-1, 0) +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 forward(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)) @@ -275,7 +285,19 @@ proc newChessboard: ChessBoard = enPassantSquare: emptyMove(), move: emptyMove(), turn: White, - fullMoveCount: 1) + fullMoveCount: 1, + pieces: (white: (king: emptyLocation(), + queens: @[], + rooks: @[], + bishops: @[], + knights: @[], + pawns: @[]), + black: (king: emptyLocation(), + queens: @[], + rooks: @[], + bishops: @[], + knights: @[], + pawns: @[]))) @@ -532,7 +554,6 @@ func getPiece*(self: ChessBoard, square: string): Piece = return self.getPiece(square.algebraicToLocation()) - func isCapture*(move: Move): bool {.inline.} = ## Returns whether the given move is a capture ## or not @@ -555,9 +576,9 @@ proc inCheck*(self: ChessBoard, color: PieceColor = None): bool = color = self.getActiveColor() case color: of White: - return self.isAttacked(self.position.pieces.white.king, color) + return self.isAttacked(self.position.pieces.white.king, Black) of Black: - return self.isAttacked(self.position.pieces.black.king, color) + return self.isAttacked(self.position.pieces.black.king, White) else: # Unreachable discard @@ -608,38 +629,40 @@ proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: if result.king: # Short castle - var location = loc + var + location = loc + otherPiece: Piece while true: location = location + kingSide if not location.isValid(): break - if self.grid[location.row, location.col].color == color: - # Blocked by own piece + otherPiece = self.grid[location.row, location.col] + + if otherPiece.color == None: + continue + + if otherPiece.color == color.opposite() or otherPiece.kind != Rook or self.isAttacked(location, color.opposite()): result.king = false break - # Square is attacked or blocked by enemy piece - if self.isAttacked(location, color) or self.grid[location.row, location.col].color != None: - result.king = false - break - # Square is occupied by our rook: we're done. No need to check the color or type of it (because - # if it weren't the right color, castling rights would've already been lost and we wouldn't - # have got this far) if location == color.kingSideRook(): break - # Square is empty and not attacked. Keep going if result.queen: # Long castle - var location = loc + var + location = loc + otherPiece: Piece while true: location = location + queenSide if not location.isValid(): break - if self.grid[location.row, location.col].color == color: - result.queen = false - break - if self.isAttacked(location, color) or self.grid[location.row, location.col].color != None: - result.queen = false + otherPiece = self.grid[location.row, location.col] + + if otherPiece.color == None: + continue + + if otherPiece.color == color.opposite() or otherPiece.kind != Rook or self.isAttacked(location, color.opposite()): + result.king = false break if location == color.queenSideRook(): break @@ -684,6 +707,17 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = if diagonal.isValid() and self.grid[diagonal.row, diagonal.col].color == piece.color.opposite() and self.grid[diagonal.row, diagonal.col].kind != King: locations.add(diagonal) flags.add(Capture) + var + newLocation: Location + newLocations: seq[Location] + let pins = self.getPinnedDirections(location) + for pin in pins: + newLocation = location + pin + # Pin direction is legal + if newLocation in locations: + newLocations.add(newLocation) + if pins.len() > 0: + locations = newLocations var targetPiece: Piece for (target, flag) in zip(locations, flags): @@ -698,10 +732,10 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = ## Generates moves for the sliding piece in the given location - var - piece = self.grid[location.row, location.col] + let piece = self.grid[location.row, location.col] doAssert piece.kind in [Bishop, Rook, Queen], &"generateSlidingMoves called on a {piece.kind}" var directions: seq[Location] = @[] + # Only check in the right directions for the chosen piece if piece.kind in [Bishop, Queen]: directions.add(piece.color.topLeftDiagonal()) @@ -713,6 +747,11 @@ 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) + if pinned.len() > 0: + # If a sliding piece is pinned then it can only + # move along the pinning direction(s) + directions = pinned for direction in directions: # Slide in this direction as long as it's possible var @@ -795,6 +834,11 @@ 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) + if pinned.len() > 0: + # Knight is pinned: can't move! + return @[] + for direction in directions: # Jump to this square let square: Location = location + direction @@ -851,11 +895,11 @@ proc getAttackers*(self: ChessBoard, square: Location, color = None): seq[Locati case color: of White: for attack in self.position.attacked.black: - if attack.dest == square: + if attack.target == square: result.add(attack.source) of Black: for attack in self.position.attacked.white: - if attack.dest == square: + if attack.target == square: result.add(attack.source) else: # Unreachable @@ -866,18 +910,18 @@ proc getAttackers*(self: ChessBoard, square: Location, color = None): seq[Locati # getAttackers) proc isAttacked*(self: ChessBoard, loc: Location, color: PieceColor = None): bool = ## Returns whether the given location is attacked - ## by the given opponent + ## by the given color var color = color if color == None: - color = self.getActiveColor() + color = self.getActiveColor().opposite() case color: - of White: - for attack in self.position.attacked.black: - if attack.dest == loc: - return true of Black: for attack in self.position.attacked.black: - if attack.dest == loc: + if attack.target == loc: + return true + of White: + for attack in self.position.attacked.white: + if attack.target == loc: return true of None: discard @@ -885,12 +929,12 @@ proc isAttacked*(self: ChessBoard, loc: Location, color: PieceColor = None): boo proc isAttacked*(self: ChessBoard, square: string): bool = ## Returns whether the given square is attacked - ## by its opponent + ## by the current return self.isAttacked(square.algebraicToLocation()) -func addAttack(self: ChessBoard, attack: tuple[source, dest: Location], color: PieceColor) {.inline.} = - if attack.source.isValid() and attack.dest.isValid(): +func addAttack(self: ChessBoard, attack: tuple[source, target: Location], color: PieceColor) {.inline.} = + if attack.source.isValid() and attack.target.isValid(): case color: of White: self.position.attacked.white.add(attack) @@ -900,18 +944,33 @@ func addAttack(self: ChessBoard, attack: tuple[source, dest: Location], color: P discard -proc updatePawnAttacks(self: ChessBoard) = +proc getPinnedDirections(self: ChessBoard, loc: Location): seq[Location] = + let piece = self.grid[loc.row, loc.col] + case piece.color: + of None: + discard + of White: + for pin in self.position.pinned.black: + if pin.target == loc: + result.add(pin.direction) + of Black: + for pin in self.position.pinned.white: + if pin.target == loc: + result.add(pin.direction) + + +proc updatePawnAttacks(self: ChessBoard) {.thread.} = ## Internal helper of updateAttackedSquares for loc in self.position.pieces.white.pawns: # Pawns are special in how they capture (i.e. the # squares they can move to do not match the squares # they can capture on. Sneaky fucks) self.addAttack((loc, loc + White.topRightDiagonal()), White) - self.addAttack((loc, loc + White.topRightDiagonal()), White) + self.addAttack((loc, loc + 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) - self.addAttack((loc, loc + Black.topRightDiagonal()), Black) + self.addAttack((loc, loc + Black.topLeftDiagonal()), Black) proc updateKingAttacks(self: ChessBoard) = @@ -951,25 +1010,27 @@ proc updateKnightAttacks(self: ChessBoard) = self.addAttack((loc, loc + Black.bottomRightKnightMove(long=false)), Black) -proc getSlidingAttacks(self: ChessBoard, loc: Location): Attacked = +proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked, pins: Pinned] = ## Internal helper of updateSlidingAttacks var directions: seq[Location] = @[] - square: Location = loc - otherPiece: Piece let piece = self.grid[loc.row, loc.col] if piece.kind in [Bishop, Queen]: directions.add(piece.color.topLeftDiagonal()) directions.add(piece.color.topRightDiagonal()) directions.add(piece.color.bottomLeftDiagonal()) directions.add(piece.color.bottomRightDiagonal()) + if piece.kind in [Queen, Rook]: directions.add(piece.color.topSide()) directions.add(piece.color.bottomSide()) directions.add(piece.color.rightSide()) directions.add(piece.color.leftSide()) + for direction in directions: - square = loc + var + square = loc + otherPiece: Piece # Slide in this direction as long as it's possible while true: square = square + direction @@ -977,46 +1038,71 @@ proc getSlidingAttacks(self: ChessBoard, loc: Location): Attacked = if not square.isValid(): break otherPiece = self.grid[square.row, square.col] - # A piece is in the way: we cannot proceed - # any further - if otherPiece.color notin [piece.color.opposite(), None]: - break - # Target square is attacked - result.add((loc, 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)) + # Empty square, keep going + if otherPiece.color == None: + continue + if otherPiece.color == piece.color.opposite and otherPiece.kind != King: + # We found an enemy piece that is not + # the enemy king. We don't break out + # immediately because we first want + # to check if we've pinned a piece + var + otherSquare: Location = square + behindPiece: Piece + while true: + otherSquare = otherSquare + direction + if not otherSquare.isValid(): + break + behindPiece = self.grid[otherSquare.row, otherSquare.col] + 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 in + # this direction relative to them (that's why we have the + # minus sign: up for us is down for them and vice versa) + result.pins.add((loc, square, -direction)) + else: + break + break proc updateSlidingAttacks(self: ChessBoard) = ## Internal helper of updateAttackedSquares - var - directions: seq[Location] - piece: Piece - # Bishops + var data: tuple[attacks: Attacked, pins: Pinned] for loc in self.position.pieces.white.bishops: - for attack in self.getSlidingAttacks(loc): - self.addAttack(attack, White) - for loc in self.position.pieces.black.bishops: - for attack in self.getSlidingAttacks(loc): - self.addAttack(attack, Black) - # Rooks + data = self.getSlidingAttacks(loc) + self.position.attacked.white.extend(data.attacks) + self.position.pinned.white.extend(data.pins) for loc in self.position.pieces.white.rooks: - for attack in self.getSlidingAttacks(loc): - self.addAttack(attack, White) - for loc in self.position.pieces.black.rooks: - for attack in self.getSlidingAttacks(loc): - self.addAttack(attack, Black) - # Queens + data = self.getSlidingAttacks(loc) + self.position.attacked.white.extend(data.attacks) + self.position.pinned.white.extend(data.pins) for loc in self.position.pieces.white.queens: - for attack in self.getSlidingAttacks(loc): - self.addAttack(attack, White) + data = self.getSlidingAttacks(loc) + self.position.attacked.white.extend(data.attacks) + self.position.pinned.white.extend(data.pins) + for loc in self.position.pieces.black.bishops: + data = self.getSlidingAttacks(loc) + self.position.attacked.black.extend(data.attacks) + self.position.pinned.black.extend(data.pins) + for loc in self.position.pieces.black.rooks: + data = self.getSlidingAttacks(loc) + self.position.attacked.black.extend(data.attacks) + self.position.pinned.black.extend(data.pins) for loc in self.position.pieces.black.queens: - for attack in self.getSlidingAttacks(loc): - self.addAttack(attack, Black) + data = self.getSlidingAttacks(loc) + self.position.attacked.black.extend(data.attacks) + self.position.pinned.black.extend(data.pins) proc updateAttackedSquares(self: ChessBoard) = ## Updates internal metadata about which squares - ## are attacked. Called internally by doMove + ## are attacked self.position.attacked.white.setLen(0) self.position.attacked.black.setLen(0) @@ -1024,6 +1110,7 @@ proc updateAttackedSquares(self: ChessBoard) = self.updatePawnAttacks() # Sliding pieces self.updateSlidingAttacks() + # Knights self.updateKnightAttacks() # Kings self.updateKingAttacks() @@ -1147,11 +1234,7 @@ proc updateLocations(self: ChessBoard, move: Move) = ## the pieces on the board after a move if move.isCapture(): self.position.captured = self.grid[move.targetSquare.row, move.targetSquare.col] - try: - self.removePiece(move.targetSquare, attack=false) - except AssertionDefect: - echo move - raise + self.removePiece(move.targetSquare, attack=false) # Update the positional metadata of the moving piece self.movePiece(move) @@ -1246,7 +1329,6 @@ proc doMove(self: ChessBoard, move: Move) = self.spawnPiece(move.targetSquare, Piece(kind: Rook, color: move.piece.color)) of PromoteToQueen: self.spawnPiece(move.targetSquare, Piece(kind: Queen, color: move.piece.color)) - else: discard let previous = self.position @@ -1493,16 +1575,35 @@ proc pretty*(self: ChessBoard): string = result &= "\x1b[0m" -proc countLegalMoves*(self: ChessBoard, ply: int, verbose: bool = false, current: int = 0): tuple[nodes: int, captures: int, castles: int, checks: int, promotions: int] = +proc perft*(self: ChessBoard, ply: int, verbose: bool = false): CountData = ## Counts (and debugs) the number of legal positions reached after ## the given number of ply + var verbose = verbose if ply == 0: - result = (1, 0, 0, 0, 0) + result = (1, 0, 0, 0, 0, 0, 0) else: - var - before: string - for move in self.generateAllMoves(): - before = self.pretty() + let moves = self.generateAllMoves() + if len(moves) == 0: + inc(result.checkmates) + for move in moves: + if verbose: + let canCastle = self.canCastle(move.piece.color) + #echo "\x1Bc" + echo &"Ply: {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 &"Turn: {move.piece.color}" + echo &"Piece: {move.piece.kind}" + echo &"Flag: {move.flag}" + 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 "Before:\n" + echo self.pretty() + try: + discard readLine(stdin) + except IOError: + discard + except EOFError: + discard self.doMove(move) case move.flag: of Capture: @@ -1511,41 +1612,30 @@ proc countLegalMoves*(self: ChessBoard, ply: int, verbose: bool = false, current inc(result.castles) of PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook: inc(result.promotions) + of EnPassant: + inc(result.enPassant) else: discard if self.inCheck(): # Opponent king is in check inc(result.checks) if verbose: - let canCastle = self.canCastle(move.piece.color) - echo "\x1Bc" - echo &"Ply: {self.position.plyFromRoot} (move {current + result.nodes + 1})" - echo &"Move: {move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()} (({move.startSquare.row}, {move.startSquare.col}) -> ({move.targetSquare.row}, {move.targetSquare.col}))" - echo &"Turn: {move.piece.color}" - echo &"Piece: {move.piece.kind}" - echo &"Flag: {move.flag}" - echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}" - echo "\nBefore:" - echo before - echo "\nNow: " + echo "Now:\n" echo self.pretty() - echo &"\n\nTotal captures: {result.captures}" - echo &"Total castles: {result.castles}" - echo &"Total checks: {result.checks}" - echo &"Total promotions: {result.promotions}" - try: discard readLine(stdin) except IOError: discard except EOFError: discard - let next = self.countLegalMoves(ply - 1, verbose, result.nodes + 1) + let next = self.perft(ply - 1, verbose) result.nodes += next.nodes result.captures += next.captures result.checks += next.checks result.promotions += next.promotions result.castles += next.castles + result.enPassant += next.enPassant + result.checkmates += next.checkmates self.undoMove(move) @@ -1606,7 +1696,7 @@ when isMainModule: when compileOption("profiler"): import nimprof - - #b = newChessboardFromFEN("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - ") - echo b.countLegalMoves(3, verbose=false) + + # b = newChessboardFromFEN("fen") + echo b.perft(4, verbose=true) echo "All tests were successful" \ No newline at end of file