Switch to better mechanism to keep track of pins

This commit is contained in:
Mattia Giambirtone 2023-10-25 22:41:04 +02:00
parent 29a554d5da
commit c79af07638
1 changed files with 207 additions and 117 deletions

View File

@ -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"