Fixes to generation and added basic debugger
This commit is contained in:
parent
8f56d9f89e
commit
84226128ff
|
@ -17,6 +17,7 @@ export matrix
|
||||||
|
|
||||||
import std/strutils
|
import std/strutils
|
||||||
import std/strformat
|
import std/strformat
|
||||||
|
import std/sequtils
|
||||||
|
|
||||||
|
|
||||||
type
|
type
|
||||||
|
@ -50,12 +51,16 @@ type
|
||||||
|
|
||||||
MoveFlag* = enum
|
MoveFlag* = enum
|
||||||
## An enumeration of move flags
|
## An enumeration of move flags
|
||||||
Default = 0'i8, # Move is a regular move
|
Default = 0'i8, # No flag
|
||||||
# Castling
|
Capture, # Move is a capture
|
||||||
|
EnPassant, # Move is an en passant
|
||||||
|
Backwards, # Move is a backward move generated by undoMove
|
||||||
|
DoublePush, # Move is a double pawn push
|
||||||
|
# Castling metadata
|
||||||
CastleLong,
|
CastleLong,
|
||||||
CastleShort,
|
CastleShort,
|
||||||
XRay, # Move is an X-ray attack
|
XRay, # Move is an X-ray attack
|
||||||
# Move is a pawn promotion
|
# Pawn promotion metadata
|
||||||
PromoteToQueen,
|
PromoteToQueen,
|
||||||
PromoteToRook,
|
PromoteToRook,
|
||||||
PromoteToBishop,
|
PromoteToBishop,
|
||||||
|
@ -125,9 +130,12 @@ func `+`*(a, b: Location): Location = (a.row + b.row, a.col + b.col)
|
||||||
func isValid*(a: Location): bool {.inline.} = a.row in 0..7 and a.col in 0..7
|
func 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 generateMoves(self: ChessBoard, location: Location): seq[Move]
|
||||||
proc isAttacked*(self: ChessBoard, loc: Location): bool
|
proc isAttacked*(self: ChessBoard, loc: Location): bool
|
||||||
proc undoLastMove*(self: ChessBoard): Move {.discardable.}
|
proc undoMove*(self: ChessBoard, move: Move): Move {.discardable.}
|
||||||
proc isLegal(self: ChessBoard, move: Move): bool
|
proc isLegal(self: ChessBoard, move: Move, keep: bool = false): bool
|
||||||
proc doMove(self: ChessBoard, move: Move)
|
proc doMove(self: ChessBoard, move: Move)
|
||||||
|
proc isLegalFast(self: ChessBoard, move: Move, keep: bool = false): bool
|
||||||
|
proc makeMoveFast*(self: ChessBoard, move: Move): Move {.discardable.}
|
||||||
|
proc pretty*(self: ChessBoard): string
|
||||||
|
|
||||||
# Due to our board layout, directions of movement are reversed for white/black so
|
# 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
|
# we need these helpers to avoid going mad with integer tuples and minus signs
|
||||||
|
@ -142,10 +150,10 @@ func topSide(color: PieceColor): Location {.inline.} = (if color == White: (-1,
|
||||||
func bottomSide(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 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))
|
func doublePush(color: PieceColor): Location {.inline.} = (if color == White: (-2, 0) else: (2, 0))
|
||||||
func longCastleKing(color: PieceColor): Location {.inline.} = (0, -2)
|
func longCastleKing: Location {.inline.} = (0, -2)
|
||||||
func shortCastleKing(color: PieceColor): Location {.inline.} = (0, 2)
|
func shortCastleKing: Location {.inline.} = (0, 2)
|
||||||
func longCastleRook(color: PieceColor): Location {.inline.} = (0, 3)
|
func longCastleRook: Location {.inline.} = (0, 3)
|
||||||
func shortCastleRook(color: PieceColor): Location {.inline.} = (0, -2)
|
func shortCastleRook: Location {.inline.} = (0, -2)
|
||||||
func kingSideRook(color: PieceColor): Location {.inline.} = (if color == White: (7, 7) else: (0, 7))
|
func kingSideRook(color: PieceColor): Location {.inline.} = (if color == White: (7, 7) else: (0, 7))
|
||||||
func queenSideRook(color: PieceColor): Location {.inline.} = (if color == White: (7, 0) else: (0, 0))
|
func queenSideRook(color: PieceColor): Location {.inline.} = (if color == White: (7, 0) else: (0, 0))
|
||||||
|
|
||||||
|
@ -155,25 +163,25 @@ func bottomLeftKnightMove(color: PieceColor, long: bool = true): Location {.inli
|
||||||
if long:
|
if long:
|
||||||
return (2, -1)
|
return (2, -1)
|
||||||
else:
|
else:
|
||||||
return (-1, -2)
|
return (1, -2)
|
||||||
elif color == Black:
|
elif color == Black:
|
||||||
if long:
|
if long:
|
||||||
return (-2, 1)
|
return (2, 1)
|
||||||
else:
|
else:
|
||||||
return (1, -2)
|
return (2, -1)
|
||||||
|
|
||||||
|
|
||||||
func bottomRightKnightMove(color: PieceColor, long: bool = true): Location {.inline.} =
|
func bottomRightKnightMove(color: PieceColor, long: bool = true): Location {.inline.} =
|
||||||
if color == White:
|
if color == White:
|
||||||
if long:
|
if long:
|
||||||
return (2, -1)
|
return (2, 1)
|
||||||
else:
|
else:
|
||||||
return (1, 2)
|
return (1, 2)
|
||||||
elif color == Black:
|
elif color == Black:
|
||||||
if long:
|
if long:
|
||||||
return (2, 1)
|
return (-2, -1)
|
||||||
else:
|
else:
|
||||||
return (1, 2)
|
return (-1, -2)
|
||||||
|
|
||||||
|
|
||||||
func topLeftKnightMove(color: PieceColor, long: bool = true): Location {.inline.} =
|
func topLeftKnightMove(color: PieceColor, long: bool = true): Location {.inline.} =
|
||||||
|
@ -199,7 +207,7 @@ func topRightKnightMove(color: PieceColor, long: bool = true): Location {.inline
|
||||||
if long:
|
if long:
|
||||||
return (2, -1)
|
return (2, -1)
|
||||||
else:
|
else:
|
||||||
return (-1, 2)
|
return (1, -2)
|
||||||
|
|
||||||
|
|
||||||
func getActiveColor*(self: ChessBoard): PieceColor {.inline.} =
|
func getActiveColor*(self: ChessBoard): PieceColor {.inline.} =
|
||||||
|
@ -266,7 +274,8 @@ proc newChessboard: ChessBoard =
|
||||||
result.position = Position(attacked: (@[], @[]),
|
result.position = Position(attacked: (@[], @[]),
|
||||||
enPassantSquare: emptyMove(),
|
enPassantSquare: emptyMove(),
|
||||||
move: emptyMove(),
|
move: emptyMove(),
|
||||||
turn: White)
|
turn: White,
|
||||||
|
fullMoveCount: 1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -347,8 +356,8 @@ proc newChessboardFromFEN*(state: string): ChessBoard =
|
||||||
column = 0
|
column = 0
|
||||||
of '0'..'9':
|
of '0'..'9':
|
||||||
# Skip x columns
|
# Skip x columns
|
||||||
let x = int(uint8(c) - uint8('0')) - 1
|
let x = int(uint8(c) - uint8('0'))
|
||||||
if x > 7:
|
if x > 8:
|
||||||
raise newException(ValueError, "invalid skip value (> 8) in FEN string")
|
raise newException(ValueError, "invalid skip value (> 8) in FEN string")
|
||||||
column += int8(x)
|
column += int8(x)
|
||||||
else:
|
else:
|
||||||
|
@ -530,7 +539,7 @@ func getCapture*(self: ChessBoard, move: Move): Location =
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
return ((if move.piece.color == White: move.targetSquare.row + 1 else: move.targetSquare.row - 1), move.targetSquare.col)
|
return ((if move.piece.color == White: move.targetSquare.row + 1 else: move.targetSquare.row - 1), move.targetSquare.col)
|
||||||
if target.color == move.piece.color.opposite():
|
elif target.color == move.piece.color.opposite():
|
||||||
return move.targetSquare
|
return move.targetSquare
|
||||||
|
|
||||||
|
|
||||||
|
@ -545,20 +554,17 @@ proc inCheck*(self: ChessBoard, color: PieceColor = None): bool =
|
||||||
## king is in check. If the color is
|
## king is in check. If the color is
|
||||||
## set to None, checks are checked
|
## set to None, checks are checked
|
||||||
## for the active color's king
|
## for the active color's king
|
||||||
|
var color = color
|
||||||
|
if color == None:
|
||||||
|
color = self.getActiveColor()
|
||||||
case color:
|
case color:
|
||||||
of White:
|
of White:
|
||||||
return self.isAttacked(self.position.pieces.white.king)
|
return self.isAttacked(self.position.pieces.white.king)
|
||||||
of Black:
|
of Black:
|
||||||
return self.isAttacked(self.position.pieces.black.king)
|
return self.isAttacked(self.position.pieces.black.king)
|
||||||
of None:
|
else:
|
||||||
case self.getActiveColor():
|
# Unreachable
|
||||||
of White:
|
discard
|
||||||
return self.isAttacked(self.position.pieces.white.king)
|
|
||||||
of Black:
|
|
||||||
return self.isAttacked(self.position.pieces.black.king)
|
|
||||||
else:
|
|
||||||
# Unreachable
|
|
||||||
discard
|
|
||||||
|
|
||||||
|
|
||||||
proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: bool] {.inline.} =
|
proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: bool] {.inline.} =
|
||||||
|
@ -647,57 +653,51 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
|
||||||
var
|
var
|
||||||
piece = self.grid[location.row, location.col]
|
piece = self.grid[location.row, location.col]
|
||||||
locations: seq[Location] = @[]
|
locations: seq[Location] = @[]
|
||||||
|
flags: seq[MoveFlag] = @[]
|
||||||
doAssert piece.kind == Pawn, &"generatePawnMoves called on a {piece.kind}"
|
doAssert piece.kind == Pawn, &"generatePawnMoves called on a {piece.kind}"
|
||||||
# Pawns can move forward one square
|
# Pawns can move forward one square
|
||||||
let forwardOffset = piece.color.forward()
|
let forwardOffset = piece.color.forward()
|
||||||
let forward = (forwardOffset + location)
|
let forward = (forwardOffset + location)
|
||||||
|
# Only if the square is empty though
|
||||||
if forward.isValid() and self.grid[forward.row, forward.col].color == None:
|
if forward.isValid() and self.grid[forward.row, forward.col].color == None:
|
||||||
locations.add(forwardOffset)
|
locations.add(forwardOffset)
|
||||||
|
flags.add(Default)
|
||||||
# If the pawn is on its first rank, it can push two squares
|
# If the pawn is on its first rank, it can push two squares
|
||||||
if location.row == piece.getStartRow():
|
if location.row == piece.getStartRow():
|
||||||
locations.add(piece.color.doublePush())
|
let doubleOffset = piece.color.doublePush()
|
||||||
|
let double = location + doubleOffset
|
||||||
|
# Check if both squares are available
|
||||||
|
if double.isValid() and self.grid[forward.row, forward.col].color == None and self.grid[double.row, double.col].color == None:
|
||||||
|
locations.add(piece.color.doublePush())
|
||||||
|
flags.add(DoublePush)
|
||||||
if self.position.enPassantSquare.piece.color == piece.color.opposite:
|
if self.position.enPassantSquare.piece.color == piece.color.opposite:
|
||||||
if abs(self.position.enPassantSquare.targetSquare.col - location.col) == 1 and abs(self.position.enPassantSquare.targetSquare.row - location.row) == 1:
|
if abs(self.position.enPassantSquare.targetSquare.col - location.col) == 1 and abs(self.position.enPassantSquare.targetSquare.row - location.row) == 1:
|
||||||
# Only viable if the piece is on the diagonal of the target
|
# Only viable if the piece is on the diagonal of the target
|
||||||
locations.add(self.position.enPassantSquare.targetSquare)
|
locations.add(self.position.enPassantSquare.targetSquare)
|
||||||
|
flags.add(EnPassant)
|
||||||
# They can also move on either diagonal one
|
# They can also move on either diagonal one
|
||||||
# square, but only to capture
|
# square, but only to capture
|
||||||
if location.col in 1..6:
|
var diagonal = piece.color.topRightDiagonal()
|
||||||
# Top right diagonal
|
if (diagonal + location).isValid() and self.isCapture(Move(piece: piece, startSquare: location, targetSquare: location + diagonal)):
|
||||||
locations.add(piece.color.topRightDiagonal())
|
locations.add(diagonal)
|
||||||
if location.row in 1..6:
|
flags.add(Capture)
|
||||||
# Top left diagonal
|
diagonal = piece.color.topLeftDiagonal()
|
||||||
locations.add(piece.color.topLeftDiagonal())
|
if (diagonal + location).isValid() and self.isCapture(Move(piece: piece, startSquare: location, targetSquare: location + diagonal)):
|
||||||
|
locations.add(diagonal)
|
||||||
|
flags.add(Capture)
|
||||||
|
|
||||||
# Pawn is at the right side, can only capture
|
|
||||||
# on the left one
|
|
||||||
if location.col == 7 and location.row < 7:
|
|
||||||
locations.add(piece.color.topLeftDiagonal())
|
|
||||||
# Pawn is at the left side, can only capture
|
|
||||||
# on the right one
|
|
||||||
if location.col == 0 and location.row < 7:
|
|
||||||
locations.add(piece.color.topRightDiagonal())
|
|
||||||
var
|
var
|
||||||
newLocation: Location
|
newLocation: Location
|
||||||
targetPiece: Piece
|
targetPiece: Piece
|
||||||
for target in locations:
|
for (target, flag) in zip(locations, flags):
|
||||||
newLocation = location + target
|
newLocation = location + target
|
||||||
if not newLocation.isValid():
|
|
||||||
continue
|
|
||||||
targetPiece = self.grid[newLocation.row, newLocation.col]
|
targetPiece = self.grid[newLocation.row, newLocation.col]
|
||||||
if targetPiece.color == piece.color:
|
|
||||||
# Can't move over a friendly piece
|
|
||||||
continue
|
|
||||||
if location.col != newLocation.col and not self.isCapture(Move(piece: piece, startSquare: location, targetSquare: newLocation)):
|
|
||||||
# Can only move diagonally when capturing
|
|
||||||
continue
|
|
||||||
if newLocation.row == piece.color.getLastRow():
|
if newLocation.row == piece.color.getLastRow():
|
||||||
# Generate all promotion moves
|
# Pawn reached the other side of the board: generate all potential piece promotions
|
||||||
for promotionType in [PromoteToKnight, PromoteToBishop, PromoteToRook, PromoteToQueen]:
|
for promotionType in [PromoteToKnight, PromoteToBishop, PromoteToRook, PromoteToQueen]:
|
||||||
result.add(Move(startSquare: location, targetSquare: newLocation, piece: piece, flag: promotionType))
|
result.add(Move(startSquare: location, targetSquare: newLocation, piece: piece, flag: promotionType))
|
||||||
continue
|
continue
|
||||||
# Move is just a pawn push
|
result.add(Move(startSquare: location, targetSquare: newLocation, piece: piece, flag: flag))
|
||||||
result.add(Move(startSquare: location, targetSquare: newLocation, piece: piece))
|
|
||||||
|
|
||||||
|
|
||||||
proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
|
proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
|
||||||
|
@ -734,7 +734,7 @@ proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
|
||||||
if otherPiece.color == piece.color.opposite:
|
if otherPiece.color == piece.color.opposite:
|
||||||
# Target square contains an enemy piece: capture
|
# Target square contains an enemy piece: capture
|
||||||
# it and stop going any further
|
# it and stop going any further
|
||||||
result.add(Move(startSquare: location, targetSquare: square, piece: piece))
|
result.add(Move(startSquare: location, targetSquare: square, piece: piece, flag: Capture))
|
||||||
break
|
break
|
||||||
# Target square is empty
|
# Target square is empty
|
||||||
result.add(Move(startSquare: location, targetSquare: square, piece: piece))
|
result.add(Move(startSquare: location, targetSquare: square, piece: piece))
|
||||||
|
@ -756,21 +756,25 @@ proc generateKingMoves(self: ChessBoard, location: Location): seq[Move] =
|
||||||
# Castling
|
# Castling
|
||||||
let canCastle = self.canCastle()
|
let canCastle = self.canCastle()
|
||||||
if canCastle.queen:
|
if canCastle.queen:
|
||||||
directions.add(piece.color.longCastleKing())
|
directions.add(longCastleKing())
|
||||||
if canCastle.king:
|
if canCastle.king:
|
||||||
directions.add(piece.color.shortCastleKing())
|
directions.add(shortCastleKing())
|
||||||
var flag = Default
|
var flag = Default
|
||||||
for direction in directions:
|
for direction in directions:
|
||||||
if direction == piece.color.longCastleKing():
|
if direction == longCastleKing():
|
||||||
flag = CastleLong
|
flag = CastleLong
|
||||||
elif direction == piece.color.shortCastleKing():
|
elif direction == shortCastleKing():
|
||||||
flag = CastleShort
|
flag = CastleShort
|
||||||
|
else:
|
||||||
|
flag = Default
|
||||||
# Step in this direction once
|
# Step in this direction once
|
||||||
let square: Location = location + direction
|
let square: Location = location + direction
|
||||||
# End of board reached
|
# End of board reached
|
||||||
if not square.isValid():
|
if not square.isValid():
|
||||||
continue
|
continue
|
||||||
let otherPiece = self.grid[square.row, square.col]
|
let otherPiece = self.grid[square.row, square.col]
|
||||||
|
if otherPiece.color == self.getActiveColor.opposite():
|
||||||
|
flag = Capture
|
||||||
# A friendly piece is in the way, move onto the next
|
# A friendly piece is in the way, move onto the next
|
||||||
# direction
|
# direction
|
||||||
if otherPiece.color == piece.color:
|
if otherPiece.color == piece.color:
|
||||||
|
@ -806,7 +810,7 @@ proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] =
|
||||||
if otherPiece.color == piece.color.opposite:
|
if otherPiece.color == piece.color.opposite:
|
||||||
# Target square contains an enemy piece: capture
|
# Target square contains an enemy piece: capture
|
||||||
# it
|
# it
|
||||||
result.add(Move(startSquare: location, targetSquare: square, piece: piece))
|
result.add(Move(startSquare: location, targetSquare: square, piece: piece, flag: Capture))
|
||||||
continue
|
continue
|
||||||
# Target square is empty
|
# Target square is empty
|
||||||
result.add(Move(startSquare: location, targetSquare: square, piece: piece))
|
result.add(Move(startSquare: location, targetSquare: square, piece: piece))
|
||||||
|
@ -829,14 +833,6 @@ proc generateMoves(self: ChessBoard, location: Location): seq[Move] =
|
||||||
return @[]
|
return @[]
|
||||||
|
|
||||||
|
|
||||||
proc generateLegalMoves*(self: ChessBoard, location: Location): seq[Move] =
|
|
||||||
## Returns the list of possible legal chess moves for the
|
|
||||||
## piece in the given location
|
|
||||||
for move in self.generateMoves(location):
|
|
||||||
if self.isLegal(move):
|
|
||||||
result.add(move)
|
|
||||||
|
|
||||||
|
|
||||||
proc generateAllMoves*(self: ChessBoard): seq[Move] =
|
proc generateAllMoves*(self: ChessBoard): seq[Move] =
|
||||||
## Returns the list of all possible pseudo-legal moves
|
## Returns the list of all possible pseudo-legal moves
|
||||||
## in the current position
|
## in the current position
|
||||||
|
@ -848,18 +844,6 @@ proc generateAllMoves*(self: ChessBoard): seq[Move] =
|
||||||
result.add(move)
|
result.add(move)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
proc countLegalMoves*(self: ChessBoard, ply: int): int =
|
|
||||||
## Counts the number of legal positions reached after
|
|
||||||
## the given number of half moves
|
|
||||||
if ply == 0:
|
|
||||||
return 1
|
|
||||||
for move in self.generateAllMoves():
|
|
||||||
if self.isLegal(move):
|
|
||||||
#echo &"{move.piece.color} {move.piece.kind} {move.startSquare.locationToAlgebraic()} {move.targetSquare.locationtoAlgebraic()}"
|
|
||||||
result += self.countLegalMoves(ply - 1)
|
|
||||||
|
|
||||||
|
|
||||||
proc getAttackers*(self: ChessBoard, square: Location): seq[Piece] =
|
proc getAttackers*(self: ChessBoard, square: Location): seq[Piece] =
|
||||||
## Returns all the attackers of the given square
|
## Returns all the attackers of the given square
|
||||||
for move in self.position.attacked.black:
|
for move in self.position.attacked.black:
|
||||||
|
@ -910,12 +894,7 @@ proc updateAttackedSquares(self: ChessBoard) =
|
||||||
## Updates internal metadata about which squares
|
## Updates internal metadata about which squares
|
||||||
## are attacked. Called internally by doMove
|
## are attacked. Called internally by doMove
|
||||||
|
|
||||||
# We refresh the attack metadata at every move. This is an
|
|
||||||
# O(1) operation, because we're only updating the length
|
|
||||||
# field without deallocating the memory, which will promptly
|
|
||||||
# be reused by us again. Neat!
|
|
||||||
self.position.attacked.white.setLen(0)
|
|
||||||
self.position.attacked.black.setLen(0)
|
|
||||||
# Go over each piece one by one and see which squares
|
# Go over each piece one by one and see which squares
|
||||||
# it currently attacks
|
# it currently attacks
|
||||||
|
|
||||||
|
@ -988,7 +967,7 @@ proc removePiece(self: ChessBoard, location: Location) =
|
||||||
of Black:
|
of Black:
|
||||||
case piece.kind:
|
case piece.kind:
|
||||||
of Pawn:
|
of Pawn:
|
||||||
self.position.pieces.black.pawns.delete(self.position.pieces.white.pawns.find(location))
|
self.position.pieces.black.pawns.delete(self.position.pieces.black.pawns.find(location))
|
||||||
of Bishop:
|
of Bishop:
|
||||||
self.position.pieces.black.bishops.delete(self.position.pieces.black.bishops.find(location))
|
self.position.pieces.black.bishops.delete(self.position.pieces.black.bishops.find(location))
|
||||||
of Knight:
|
of Knight:
|
||||||
|
@ -1005,10 +984,11 @@ proc removePiece(self: ChessBoard, location: Location) =
|
||||||
discard
|
discard
|
||||||
|
|
||||||
|
|
||||||
proc movePiece(self: ChessBoard, move: Move) =
|
proc movePiece(self: ChessBoard, move: Move, attack: bool = true) =
|
||||||
## Internal helper to move a piece. Does
|
## Internal helper to move a piece. If attack
|
||||||
## not update attacked squares, just position
|
## is set to false, then this function does
|
||||||
## metadata and the grid itself
|
## not update attacked squares metadata, just
|
||||||
|
## positional info and the grid itself
|
||||||
case move.piece.color:
|
case move.piece.color:
|
||||||
of White:
|
of White:
|
||||||
case move.piece.kind:
|
case move.piece.kind:
|
||||||
|
@ -1060,17 +1040,26 @@ proc movePiece(self: ChessBoard, move: Move) =
|
||||||
self.grid[move.startSquare.row, move.startSquare.col] = emptyPiece()
|
self.grid[move.startSquare.row, move.startSquare.col] = emptyPiece()
|
||||||
# Actually move the piece
|
# Actually move the piece
|
||||||
self.grid[move.targetSquare.row, move.targetSquare.col] = move.piece
|
self.grid[move.targetSquare.row, move.targetSquare.col] = move.piece
|
||||||
|
if attack:
|
||||||
|
self.updateAttackedSquares()
|
||||||
|
|
||||||
|
|
||||||
proc updatePositions(self: ChessBoard, move: Move) =
|
proc movePiece(self: ChessBoard, startSquare, targetSquare: Location, attack: bool = true) =
|
||||||
|
## Like the other movePiece(), but with two locations
|
||||||
|
self.movePiece(Move(startSquare: startSquare, targetSquare: targetSquare,
|
||||||
|
piece: self.grid[startSquare.row, startSquare.col],
|
||||||
|
),
|
||||||
|
attack
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
proc updateLocations(self: ChessBoard, move: Move) =
|
||||||
## Internal helper to update the position of
|
## Internal helper to update the position of
|
||||||
## the pieces on the board after a move
|
## the pieces on the board after a move
|
||||||
let capture = self.getCapture(move)
|
let capture = self.getCapture(move)
|
||||||
if capture != emptyLocation():
|
if capture != emptyLocation():
|
||||||
self.position.captured = self.grid[capture.row, capture.col]
|
self.position.captured = self.grid[capture.row, capture.col]
|
||||||
if capture != self.getEnPassantTarget():
|
self.removePiece(capture)
|
||||||
# En passant is handled elsewhere
|
|
||||||
self.removePiece(capture)
|
|
||||||
# Update the positional metadata of the moving piece
|
# Update the positional metadata of the moving piece
|
||||||
self.movePiece(move)
|
self.movePiece(move)
|
||||||
|
|
||||||
|
@ -1083,7 +1072,7 @@ proc doMove(self: ChessBoard, move: Move) =
|
||||||
|
|
||||||
# Final checks
|
# Final checks
|
||||||
|
|
||||||
# Record the move in the position
|
# Record the final move in the position
|
||||||
self.position.move = move
|
self.position.move = move
|
||||||
|
|
||||||
# Needed to detect draw by the 50 move rule
|
# Needed to detect draw by the 50 move rule
|
||||||
|
@ -1091,110 +1080,115 @@ proc doMove(self: ChessBoard, move: Move) =
|
||||||
halfMoveClock = self.position.halfMoveClock
|
halfMoveClock = self.position.halfMoveClock
|
||||||
fullMoveCount = self.position.fullMoveCount
|
fullMoveCount = self.position.fullMoveCount
|
||||||
castlingAvailable = self.position.castlingAvailable
|
castlingAvailable = self.position.castlingAvailable
|
||||||
if move.piece.kind == Pawn or self.isCapture(move):
|
if move.flag != Backwards:
|
||||||
halfMoveClock = 0
|
if move.piece.kind == Pawn or self.isCapture(move):
|
||||||
else:
|
halfMoveClock = 0
|
||||||
inc(halfMoveClock)
|
|
||||||
if move.piece.color == Black:
|
|
||||||
inc(fullMoveCount)
|
|
||||||
# Castling check: have the rooks moved?
|
|
||||||
if move.piece.kind == Rook:
|
|
||||||
case move.piece.color:
|
|
||||||
of White:
|
|
||||||
if move.startSquare.row == move.piece.getStartRow():
|
|
||||||
if move.startSquare.col == 0:
|
|
||||||
# Queen side
|
|
||||||
castlingAvailable.white.queen = false
|
|
||||||
elif move.startSquare.col == 7:
|
|
||||||
# King side
|
|
||||||
castlingAvailable.white.king = false
|
|
||||||
of Black:
|
|
||||||
if move.startSquare.row == move.piece.getStartRow():
|
|
||||||
if move.startSquare.col == 0:
|
|
||||||
# Queen side
|
|
||||||
castlingAvailable.black.queen = false
|
|
||||||
elif move.startSquare.col == 7:
|
|
||||||
# King side
|
|
||||||
castlingAvailable.black.king = false
|
|
||||||
else:
|
|
||||||
discard
|
|
||||||
# Has a rook been captured?
|
|
||||||
let capture = self.getCapture(move)
|
|
||||||
if capture != emptyLocation():
|
|
||||||
let piece = self.grid[capture.row, capture.col]
|
|
||||||
if piece.kind == Rook:
|
|
||||||
case piece.color:
|
|
||||||
of White:
|
|
||||||
if capture == piece.color.queenSideRook():
|
|
||||||
# Queen side
|
|
||||||
castlingAvailable.white.queen = false
|
|
||||||
elif capture == piece.color.kingSideRook():
|
|
||||||
# King side
|
|
||||||
castlingAvailable.white.king = false
|
|
||||||
of Black:
|
|
||||||
if capture == piece.color.queenSideRook():
|
|
||||||
# Queen side
|
|
||||||
castlingAvailable.black.queen = false
|
|
||||||
elif capture == piece.color.kingSideRook():
|
|
||||||
# King side
|
|
||||||
castlingAvailable.black.king = false
|
|
||||||
else:
|
|
||||||
# Unreachable
|
|
||||||
discard
|
|
||||||
# Has the king moved?
|
|
||||||
if move.piece.kind == King:
|
|
||||||
case move.piece.color:
|
|
||||||
of White:
|
|
||||||
castlingAvailable.white.king = false
|
|
||||||
castlingAvailable.white.queen = false
|
|
||||||
of Black:
|
|
||||||
castlingAvailable.black.king = false
|
|
||||||
castlingAvailable.black.queen = false
|
|
||||||
else:
|
|
||||||
discard
|
|
||||||
# Update position and attack metadata
|
|
||||||
self.updatePositions(move)
|
|
||||||
self.updateAttackedSquares()
|
|
||||||
|
|
||||||
var location: Location
|
|
||||||
if move.flag in [CastleShort, CastleLong]:
|
|
||||||
# Move the rook onto the
|
|
||||||
# correct file
|
|
||||||
var
|
|
||||||
location: Location
|
|
||||||
target: Location
|
|
||||||
if move.flag == CastleShort:
|
|
||||||
location = move.piece.color.kingSideRook()
|
|
||||||
target = move.piece.color.shortCastleRook()
|
|
||||||
else:
|
else:
|
||||||
location = move.piece.color.queenSideRook()
|
inc(halfMoveClock)
|
||||||
target = move.piece.color.longCastleRook()
|
if move.piece.color == Black:
|
||||||
let rook = self.grid[location.row, location.col]
|
inc(fullMoveCount)
|
||||||
let move = Move(startSquare: location, targetSquare: location + target, piece: rook, flag: move.flag)
|
# Castling check: have the rooks moved?
|
||||||
self.updatePositions(move)
|
if move.piece.kind == Rook:
|
||||||
self.updateAttackedSquares()
|
case move.piece.color:
|
||||||
|
of White:
|
||||||
|
if move.startSquare.row == move.piece.getStartRow():
|
||||||
|
if move.startSquare.col == 0:
|
||||||
|
# Queen side
|
||||||
|
castlingAvailable.white.queen = false
|
||||||
|
elif move.startSquare.col == 7:
|
||||||
|
# King side
|
||||||
|
castlingAvailable.white.king = false
|
||||||
|
of Black:
|
||||||
|
if move.startSquare.row == move.piece.getStartRow():
|
||||||
|
if move.startSquare.col == 0:
|
||||||
|
# Queen side
|
||||||
|
castlingAvailable.black.queen = false
|
||||||
|
elif move.startSquare.col == 7:
|
||||||
|
# King side
|
||||||
|
castlingAvailable.black.king = false
|
||||||
|
else:
|
||||||
|
discard
|
||||||
|
# Has a rook been captured?
|
||||||
|
let capture = self.getCapture(move)
|
||||||
|
if capture != emptyLocation():
|
||||||
|
let piece = self.grid[capture.row, capture.col]
|
||||||
|
if piece.kind == Rook:
|
||||||
|
case piece.color:
|
||||||
|
of White:
|
||||||
|
if capture == piece.color.queenSideRook():
|
||||||
|
# Queen side
|
||||||
|
castlingAvailable.white.queen = false
|
||||||
|
elif capture == piece.color.kingSideRook():
|
||||||
|
# King side
|
||||||
|
castlingAvailable.white.king = false
|
||||||
|
of Black:
|
||||||
|
if capture == piece.color.queenSideRook():
|
||||||
|
# Queen side
|
||||||
|
castlingAvailable.black.queen = false
|
||||||
|
elif capture == piece.color.kingSideRook():
|
||||||
|
# King side
|
||||||
|
castlingAvailable.black.king = false
|
||||||
|
else:
|
||||||
|
# Unreachable
|
||||||
|
discard
|
||||||
|
# Has the king moved?
|
||||||
|
if move.piece.kind == King:
|
||||||
|
case move.piece.color:
|
||||||
|
of White:
|
||||||
|
castlingAvailable.white.king = false
|
||||||
|
castlingAvailable.white.queen = false
|
||||||
|
of Black:
|
||||||
|
castlingAvailable.black.king = false
|
||||||
|
castlingAvailable.black.queen = false
|
||||||
|
else:
|
||||||
|
discard
|
||||||
|
|
||||||
|
let previous = self.position
|
||||||
|
if move.flag != Backwards:
|
||||||
|
# Record final position for future reference
|
||||||
|
self.positions.add(previous)
|
||||||
|
# Create new position
|
||||||
|
self.position = Position(plyFromRoot: self.position.plyFromRoot + 1,
|
||||||
|
halfMoveClock: halfMoveClock,
|
||||||
|
fullMoveCount: fullMoveCount,
|
||||||
|
captured: emptyPiece(),
|
||||||
|
turn: self.getActiveColor().opposite,
|
||||||
|
castlingAvailable: castlingAvailable,
|
||||||
|
# Updated at the next call to doMove()
|
||||||
|
move: emptyMove(),
|
||||||
|
pieces: previous.pieces,
|
||||||
|
)
|
||||||
|
|
||||||
|
var location: Location
|
||||||
|
if move.flag in [CastleShort, CastleLong]:
|
||||||
|
# Move the rook onto the
|
||||||
|
# correct file
|
||||||
|
var
|
||||||
|
location: Location
|
||||||
|
target: Location
|
||||||
|
if move.flag == CastleShort:
|
||||||
|
location = move.piece.color.kingSideRook()
|
||||||
|
target = shortCastleRook()
|
||||||
|
else:
|
||||||
|
location = move.piece.color.queenSideRook()
|
||||||
|
target = longCastleRook()
|
||||||
|
let rook = self.grid[location.row, location.col]
|
||||||
|
let move = Move(startSquare: location, targetSquare: location + target, piece: rook, flag: move.flag)
|
||||||
|
self.movePiece(move, attack=false)
|
||||||
|
|
||||||
|
# Update position and attack metadata
|
||||||
|
if move.flag == Backwards:
|
||||||
|
self.movePiece(move.targetSquare, move.startSquare)
|
||||||
|
else:
|
||||||
|
self.movePiece(move)
|
||||||
|
|
||||||
# Record final position for future reference
|
|
||||||
self.positions.add(self.position)
|
|
||||||
# Create new position with
|
|
||||||
var newPos = Position(plyFromRoot: self.position.plyFromRoot + 1,
|
|
||||||
halfMoveClock: halfMoveClock,
|
|
||||||
fullMoveCount: fullMoveCount,
|
|
||||||
captured: emptyPiece(),
|
|
||||||
turn: self.getActiveColor().opposite,
|
|
||||||
castlingAvailable: castlingAvailable,
|
|
||||||
# Updated at the next call to doMove()
|
|
||||||
move: emptyMove(),
|
|
||||||
)
|
|
||||||
# Check for double pawn push
|
# Check for double pawn push
|
||||||
if move.piece.kind == Pawn and abs(move.startSquare.row - move.targetSquare.row) == 2:
|
if move.flag == DoublePush:
|
||||||
newPos.enPassantSquare = Move(piece: move.piece,
|
self.position.enPassantSquare = Move(piece: move.piece,
|
||||||
startSquare: (move.startSquare.row, move.startSquare.col),
|
startSquare: (move.startSquare.row, move.startSquare.col),
|
||||||
targetSquare: move.targetSquare + move.piece.color.bottomSide())
|
targetSquare: move.targetSquare + move.piece.color.bottomSide())
|
||||||
else:
|
else:
|
||||||
newPos.enPassantSquare = emptyMove()
|
self.position.enPassantSquare = emptyMove()
|
||||||
|
|
||||||
self.position = newPos
|
|
||||||
|
|
||||||
|
|
||||||
proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) =
|
proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) =
|
||||||
|
@ -1242,41 +1236,59 @@ proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) =
|
||||||
self.grid[location.row, location.col] = piece
|
self.grid[location.row, location.col] = piece
|
||||||
|
|
||||||
|
|
||||||
proc undoLastMove*(self: ChessBoard): Move {.discardable.} =
|
proc undoMove*(self: ChessBoard, move: Move): Move {.discardable.} =
|
||||||
## Undoes the last move, restoring the previous position.
|
## Undoes the given move if possible
|
||||||
## If there are no positions to roll back to to, this is a
|
|
||||||
## no-op. Returns the move that was performed (may be empty)
|
|
||||||
result = emptyMove()
|
result = emptyMove()
|
||||||
if self.positions.len() == 0:
|
if self.positions.len() == 0:
|
||||||
return
|
return
|
||||||
var
|
var move: Move = move
|
||||||
previous = self.positions[^1]
|
let previous = self.positions.pop()
|
||||||
oppositeMove = Move(piece: previous.move.piece, targetSquare: previous.move.startSquare, startSquare: previous.move.targetSquare)
|
move.flag = Backwards
|
||||||
self.removePiece(previous.move.startSquare)
|
self.doMove(move)
|
||||||
self.position = previous
|
self.position = previous
|
||||||
if previous.move != emptyMove():
|
self.position.move = move
|
||||||
self.spawnPiece(previous.move.startSquare, previous.move.piece)
|
#self.updateLocations(move)
|
||||||
self.updateAttackedSquares()
|
return move
|
||||||
self.updatePositions(oppositeMove)
|
|
||||||
if previous.captured != emptyPiece():
|
|
||||||
self.spawnPiece(previous.move.targetSquare, previous.captured)
|
|
||||||
discard self.positions.pop()
|
|
||||||
return self.position.move
|
|
||||||
|
|
||||||
|
|
||||||
proc isLegal(self: ChessBoard, move: Move): bool =
|
proc isLegal(self: ChessBoard, move: Move, keep: bool = false): bool =
|
||||||
## Returns whether the given move is legal
|
## Returns whether the given move is legal
|
||||||
var move = move
|
var move = move
|
||||||
if move.piece.kind == King and move.piece.color.longCastleKing() + move.startSquare == move.targetSquare:
|
if move.piece.kind == King and longCastleKing() + move.startSquare == move.targetSquare:
|
||||||
move.flag = CastleLong
|
move.flag = CastleLong
|
||||||
elif move.piece.kind == King and move.piece.color.shortCastleKing() + move.startSquare == move.targetSquare:
|
elif move.piece.kind == King and shortCastleKing() + move.startSquare == move.targetSquare:
|
||||||
move.flag = CastleShort
|
move.flag = CastleShort
|
||||||
|
if move.piece.kind == Pawn and move.piece.color.doublePush() + move.startSquare == move.targetSquare:
|
||||||
|
move.flag = DoublePush
|
||||||
if move notin self.generateMoves(move.startSquare):
|
if move notin self.generateMoves(move.startSquare):
|
||||||
# Piece cannot arrive to destination (blocked
|
# Piece cannot arrive to destination (blocked
|
||||||
# or otherwise invalid move)
|
# or otherwise invalid move)
|
||||||
return false
|
return false
|
||||||
self.doMove(move)
|
self.doMove(move)
|
||||||
defer: self.undoLastMove()
|
if not keep:
|
||||||
|
defer: self.undoMove(move)
|
||||||
|
# Move would reveal an attack
|
||||||
|
# on our king: not allowed
|
||||||
|
if self.inCheck(move.piece.color):
|
||||||
|
return false
|
||||||
|
# All checks have passed: move is legal
|
||||||
|
result = true
|
||||||
|
|
||||||
|
|
||||||
|
proc isLegalFast(self: ChessBoard, move: Move, keep: bool = false): bool =
|
||||||
|
## Returns whether the given move is legal
|
||||||
|
## assuming that the input move is pseudo legal
|
||||||
|
var move = move
|
||||||
|
if move.piece.kind == King and longCastleKing() + move.startSquare == move.targetSquare:
|
||||||
|
move.flag = CastleLong
|
||||||
|
elif move.piece.kind == King and shortCastleKing() + move.startSquare == move.targetSquare:
|
||||||
|
move.flag = CastleShort
|
||||||
|
if move.piece.kind == Pawn and move.piece.color.doublePush() + move.startSquare == move.targetSquare:
|
||||||
|
move.flag = DoublePush
|
||||||
|
self.position.move = move
|
||||||
|
self.doMove(move)
|
||||||
|
if not keep:
|
||||||
|
defer: self.undoMove(move)
|
||||||
# Move would reveal an attack
|
# Move would reveal an attack
|
||||||
# on our king: not allowed
|
# on our king: not allowed
|
||||||
if self.inCheck(move.piece.color):
|
if self.inCheck(move.piece.color):
|
||||||
|
@ -1287,11 +1299,26 @@ proc isLegal(self: ChessBoard, move: Move): bool =
|
||||||
|
|
||||||
proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} =
|
proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} =
|
||||||
## Like the other makeMove(), but with a Move object
|
## Like the other makeMove(), but with a Move object
|
||||||
|
|
||||||
result = move
|
result = move
|
||||||
if not self.isLegal(move):
|
self.position.move = move
|
||||||
|
if move.flag == Backwards:
|
||||||
|
self.doMove(result)
|
||||||
|
let legal = self.isLegal(move, keep=true)
|
||||||
|
if not legal:
|
||||||
return emptyMove()
|
return emptyMove()
|
||||||
self.doMove(result)
|
result = self.position.move
|
||||||
|
|
||||||
|
|
||||||
|
proc makeMoveFast*(self: ChessBoard, move: Move): Move {.discardable.} =
|
||||||
|
## Like the other makeMove(), but uses isLegalFast
|
||||||
|
result = move
|
||||||
|
self.position.move = move
|
||||||
|
if move.flag == Backwards:
|
||||||
|
self.doMove(result)
|
||||||
|
let legal = self.isLegalFast(move, keep=true)
|
||||||
|
if not legal:
|
||||||
|
return emptyMove()
|
||||||
|
result = self.position.move
|
||||||
|
|
||||||
|
|
||||||
proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.discardable.} =
|
proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.discardable.} =
|
||||||
|
@ -1360,6 +1387,63 @@ proc pretty*(self: ChessBoard): string =
|
||||||
result &= "\x1b[0m"
|
result &= "\x1b[0m"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
proc countLegalMoves*(self: ChessBoard, ply: int, verbose: bool = false, verboseIllegal: bool = false, current: int = 0): int =
|
||||||
|
## Counts (and debugs) the number of legal positions reached after
|
||||||
|
## the given number of half moves
|
||||||
|
if ply == 0:
|
||||||
|
result = 1
|
||||||
|
else:
|
||||||
|
var now: string
|
||||||
|
for move in self.generateAllMoves():
|
||||||
|
now = self.pretty()
|
||||||
|
if self.isLegalFast(move, keep=true):
|
||||||
|
if verbose:
|
||||||
|
let canCastle = self.canCastle()
|
||||||
|
echo "\x1Bc"
|
||||||
|
echo &"Ply: {self.position.plyFromRoot} (move {current + result + 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 &"In check: {(if self.inCheck(move.piece.color): \"yes\" else: \"no\")}"
|
||||||
|
echo "Legal: yes"
|
||||||
|
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 now
|
||||||
|
echo "\nNow: "
|
||||||
|
echo self.pretty()
|
||||||
|
try:
|
||||||
|
discard readLine(stdin)
|
||||||
|
except IOError:
|
||||||
|
discard
|
||||||
|
except EOFError:
|
||||||
|
discard
|
||||||
|
result += self.countLegalMoves(ply - 1, verbose, verboseIllegal, result)
|
||||||
|
elif verboseIllegal:
|
||||||
|
let canCastle = self.canCastle()
|
||||||
|
echo "\x1Bc"
|
||||||
|
echo &"Ply: {self.position.plyFromRoot} (move {current + result + 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 &"In check: {(if self.inCheck(move.piece.color): \"yes\" else: \"no\")}"
|
||||||
|
echo "Legal: no"
|
||||||
|
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 now
|
||||||
|
echo "\nNow: "
|
||||||
|
echo self.pretty()
|
||||||
|
try:
|
||||||
|
discard readLine(stdin)
|
||||||
|
except IOError:
|
||||||
|
discard
|
||||||
|
except EOFError:
|
||||||
|
discard
|
||||||
|
self.undoMove(move)
|
||||||
|
|
||||||
when isMainModule:
|
when isMainModule:
|
||||||
proc testPiece(piece: Piece, kind: PieceKind, color: PieceColor) =
|
proc testPiece(piece: Piece, kind: PieceKind, color: PieceColor) =
|
||||||
doAssert piece.kind == kind and piece.color == color, &"expected piece of kind {kind} and color {color}, got {piece.kind} / {piece.color} instead"
|
doAssert piece.kind == kind and piece.color == color, &"expected piece of kind {kind} and color {color}, got {piece.kind} / {piece.color} instead"
|
||||||
|
@ -1417,6 +1501,6 @@ when isMainModule:
|
||||||
when compileOption("profiler"):
|
when compileOption("profiler"):
|
||||||
import nimprof
|
import nimprof
|
||||||
|
|
||||||
echo b.countLegalMoves(3)
|
b = newChessboardFromFEN("8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - -")
|
||||||
|
echo b.countLegalMoves(2, verbose=true, verboseIllegal=true)
|
||||||
echo "All tests were successful"
|
echo "All tests were successful"
|
Loading…
Reference in New Issue