Castling functional

This commit is contained in:
Mattia Giambirtone 2023-10-17 22:16:01 +02:00
parent afff1db88f
commit 31f77fa22d
2 changed files with 322 additions and 164 deletions

View File

@ -26,7 +26,6 @@ type
Pieces = tuple[king: Location, queens: seq[Location], rooks: seq[Location],
bishops: seq[Location], knights: seq[Location],
pawns: seq[Location]]
Castling* = tuple[white, black: tuple[queen, king: bool]]
PieceColor* = enum
## A piece color enumeration
@ -52,6 +51,9 @@ type
MoveFlag* = enum
## An enumeration of move flags
Default, # Move is a regular move
# Castling
CastleLong,
CastleShort,
XRay, # Move is an X-ray attack
# Move is a pawn promotion
PromoteToQueen,
@ -69,8 +71,8 @@ type
Position* = ref object
## A chess position
move: Move
# Stores castling metadata
castling: Castling
# Did the rooks on either side/the king move?
castlingAvailable: tuple[white, black: tuple[queen, king: bool]]
# Number of half-moves that were performed
# to reach this position starting from the
# root of the tree
@ -114,75 +116,85 @@ func emptyPiece*: Piece {.inline.} = Piece(kind: Empty, color: None)
func emptyLocation*: Location {.inline.} = (-1 , -1)
func opposite*(c: PieceColor): PieceColor {.inline.} = (if c == White: Black else: White)
proc algebraicToLocation*(s: string): Location {.inline.}
proc getCapture*(self: ChessBoard, move: Move): Location
func getCapture*(self: ChessBoard, move: Move): Location
proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move
proc makeMove*(self: ChessBoard, move: Move): Move
proc makeMove*(self: ChessBoard, startSquare, targetSquare: Location): Move
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 isValid*(a: Location): bool = 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): bool
proc undoLastMove*(self: ChessBoard): Move
proc isLegal(self: ChessBoard, move: Move): bool
# 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
# everywhere
func topLeftDiagonal(piece: Piece): Location {.inline.} = (if piece.color == White: (-1, -1) else: (1, 1))
func topRightDiagonal(piece: Piece): Location {.inline.} = (if piece.color == White: (-1, 1) else: (1, -1))
func bottomLeftDiagonal(piece: Piece): Location {.inline.} = (if piece.color == White: (1, -1) else: (-1, 1))
func bottomRightDiagonal(piece: Piece): Location {.inline.} = (if piece.color == White: (1, 1) else: (-1, -1))
func leftSide(piece: Piece): Location {.inline.} = (if piece.color == White: (0, -1) else: (0, 1))
func rightSide(piece: Piece): Location {.inline.} = (if piece.color == White: (0, 1) else: (0, -1))
func topSide(piece: Piece): Location {.inline.} = (if piece.color == White: (-1, 0) else: (1, 0))
func bottomSide(piece: Piece): Location {.inline.} = (if piece.color == White: (1, 0) else: (-1, 0))
func forward(piece: Piece): Location {.inline.} = (if piece.color == White: (-1, 0) else: (1, 0))
func doublePush(piece: Piece): Location {.inline.} = (if piece.color == White: (-2, 0) else: (2, 0))
func topLeftDiagonal(color: PieceColor): Location {.inline.} = (if color == White: (-1, -1) else: (1, 1))
func topRightDiagonal(color: PieceColor): Location {.inline.} = (if color == White: (-1, 1) else: (1, -1))
func bottomLeftDiagonal(color: PieceColor): Location {.inline.} = (if color == White: (1, -1) else: (-1, 1))
func bottomRightDiagonal(color: PieceColor): Location {.inline.} = (if color == White: (1, 1) else: (-1, -1))
func leftSide(color: PieceColor): Location {.inline.} = (if color == White: (0, -1) else: (0, 1))
func rightSide(color: PieceColor): Location {.inline.} = (if color == White: (0, 1) else: (0, -1))
func topSide(color: PieceColor): Location {.inline.} = (if color == White: (-1, 0) else: (1, 0))
func bottomSide(color: PieceColor): Location {.inline.} = (if color == White: (1, 0) else: (-1, 0))
func 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 longCastleKing(color: PieceColor): Location {.inline.} = (0, -2)
func shortCastleKing(color: PieceColor): Location {.inline.} = (0, 2)
func longCastleRook(color: PieceColor): Location {.inline.} = (0, 3)
func shortCastleRook(color: PieceColor): Location {.inline.} = (0, -2)
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 bottomLeftKnightMove(piece: Piece, long: bool = true): Location {.inline.} =
if piece.color == White:
func bottomLeftKnightMove(color: PieceColor, long: bool = true): Location {.inline.} =
if color == White:
if long:
return (-2, 1)
else:
return (1, -2)
elif piece.color == Black:
elif color == Black:
if long:
return (2, -1)
else:
return (1, -2)
func bottomRightKnightMove(piece: Piece, long: bool = true): Location {.inline.} =
if piece.color == White:
func bottomRightKnightMove(color: PieceColor, long: bool = true): Location {.inline.} =
if color == White:
if long:
return (2, -1)
else:
return (1, 2)
elif piece.color == Black:
elif color == Black:
if long:
return (2, 1)
else:
return (1, 2)
func topLeftKnightMove(piece: Piece, long: bool = true): Location {.inline.} =
if piece.color == White:
func topLeftKnightMove(color: PieceColor, long: bool = true): Location {.inline.} =
if color == White:
if long:
return (-2, -1)
else:
return (-1, -2)
elif piece.color == Black:
elif color == Black:
if long:
return (2, 1)
else:
return (1, 2)
func topRightKnightMove(piece: Piece, long: bool = true): Location {.inline.} =
if piece.color == White:
func topRightKnightMove(color: PieceColor, long: bool = true): Location {.inline.} =
if color == White:
if long:
return (-2, 1)
else:
return (-1, 2)
elif piece.color == Black:
elif color == Black:
if long:
return (2, -1)
else:
@ -195,18 +207,6 @@ proc getActiveColor*(self: ChessBoard): PieceColor =
return self.position.turn
proc getCastlingInformation*(self: ChessBoard): tuple[queen, king: bool] =
## Returns whether castling is possible
## for the given color
case self.getActiveColor():
of White:
return self.position.castling.white
of Black:
return self.position.castling.black
else:
discard
proc getEnPassantTarget*(self: ChessBoard): Location =
## Returns the current en passant target square
return self.position.enPassantSquare.targetSquare
@ -236,7 +236,7 @@ func getStartRow(piece: Piece): int {.inline.} =
of Pawn:
return 6
else:
return 5
return 7
of Black:
case piece.kind:
of Pawn:
@ -370,13 +370,13 @@ proc newChessboardFromFEN*(state: string): ChessBoard =
# by default
discard
of 'K':
result.position.castling.white.king = true
result.position.castlingAvailable.white.king = true
of 'Q':
result.position.castling.white.queen = true
result.position.castlingAvailable.white.queen = true
of 'k':
result.position.castling.black.king = true
result.position.castlingAvailable.black.king = true
of 'q':
result.position.castling.black.queen = true
result.position.castlingAvailable.black.queen = true
else:
raise newException(ValueError, "invalid castling availability in FEN string")
of 3:
@ -503,20 +503,20 @@ proc algebraicToLocation*(s: string): Location =
return (file, rank)
proc locationToAlgebraic*(loc: Location): string =
func locationToAlgebraic*(loc: Location): string {.inline.} =
## Converts a location from our internal row, column
## notation to a square in algebraic notation
return &"{char(uint8(loc.col) + uint8('a'))}{rowToRank(loc.row)}"
proc getPiece*(self: ChessBoard, square: string): Piece =
func getPiece*(self: ChessBoard, square: string): Piece =
## Gets the piece on the given square
## in algebraic notation
let loc = square.algebraicToLocation()
return self.grid[loc.row, loc.col]
proc getCapture*(self: ChessBoard, move: Move): Location =
func getCapture*(self: ChessBoard, move: Move): Location =
## Returns the location that would be captured if this
## move were played on the board, taking en passant and
## other things into account (the move is assumed to be
@ -533,12 +533,113 @@ proc getCapture*(self: ChessBoard, move: Move): Location =
return move.targetSquare
proc isCapture*(self: ChessBoard, move: Move): bool {.inline.} =
func isCapture*(self: ChessBoard, move: Move): bool {.inline.} =
## Returns whether the given move is a capture
## or not
return self.getCapture(move) != emptyLocation()
proc inCheck*(self: ChessBoard, color: PieceColor = None): bool =
## Returns whether the given color's
## king is in check. If the color is
## set to None, checks are checked
## for the active color's king
case color:
of White:
return self.isAttacked(self.position.pieces.white.king)
of Black:
return self.isAttacked(self.position.pieces.black.king)
of None:
case self.getActiveColor():
of White:
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.} =
## Returns the sides on which castling is allowed
## for the given color. If the color is Empty, the
## currently active color is used
var color = color
if color == None:
color = self.getActiveColor()
# If the rooks or king have been moved, castling
# rights have been lost
case color:
of White:
result.king = self.position.castlingAvailable.white.king
result.queen = self.position.castlingAvailable.white.queen
of Black:
result.king = self.position.castlingAvailable.black.king
result.queen = self.position.castlingAvailable.black.queen
of None:
# Unreachable
discard
var
loc: Location
queenSide: Location
kingSide: Location
# If the path between the king and a rook is blocked, then castling
# is prohibited on that side
case color:
of White:
loc = self.position.pieces.white.king
queenSide = color.leftSide()
kingSide = color.rightSide()
of Black:
loc = self.position.pieces.black.king
queenSide = color.rightSide()
kingSide = color.leftSide()
of None:
# Unreachable
discard
if result.king:
# Short castle
var location = loc
while true:
location = location + kingSide
if not location.isValid():
break
if self.grid[location.row, location.col].kind == Empty:
continue
if location == color.kingSideRook() and self.grid[location.row, location.col].kind == Rook:
break
# Blocked by a piece
result.king = false
break
if result.queen:
# Long castle
var location = loc
while true:
location = location + queenSide
if not location.isValid():
break
if self.grid[location.row, location.col].kind == Empty:
continue
if location == color.queenSideRook() and self.grid[location.row, location.col].kind == Rook:
break
# Blocked by a piece
result.queen = false
break
# If the castling king would walk into, through or out of check
# while castling on a given side, then it is not possible to castle
# on that side until the threat exists
if (result.king or result.queen) and self.inCheck(color):
# Only check for checks if castling is still available
# by this point (if we can avoid calls to generateMoves,
# we should)
return
# TODO: Check for attacks in the various other squares
proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
## Generates the possible moves for the pawn in the given
## location
@ -547,13 +648,13 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
locations: seq[Location] = @[]
doAssert piece.kind == Pawn, &"generatePawnMoves called on a {piece.kind}"
# Pawns can move forward one square
let forwardOffset = piece.forward()
let forwardOffset = piece.color.forward()
let forward = (forwardOffset + location)
if forward.isValid() and self.grid[forward.row, forward.col].color == None:
locations.add(forwardOffset)
# If the pawn is on its first rank, it can push two squares
if location.row == piece.getStartRow():
locations.add(piece.doublePush())
locations.add(piece.color.doublePush())
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:
# Only viable if the piece is on the diagonal of the target
@ -562,19 +663,19 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
# square, but only to capture
if location.col in 1..6:
# Top right diagonal
locations.add(piece.topRightDiagonal())
locations.add(piece.color.topRightDiagonal())
if location.row in 1..6:
# Top left diagonal
locations.add(piece.topLeftDiagonal())
locations.add(piece.color.topLeftDiagonal())
# Pawn is at the right side, can only capture
# on the left one
if location.col == 7 and location.row < 7:
locations.add(piece.topLeftDiagonal())
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.topRightDiagonal())
locations.add(piece.color.topRightDiagonal())
var
newLocation: Location
targetPiece: Piece
@ -606,15 +707,15 @@ proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
var directions: seq[Location] = @[]
# Only check in the right directions for the chosen piece
if piece.kind in [Bishop, Queen]:
directions.add(piece.topLeftDiagonal())
directions.add(piece.topRightDiagonal())
directions.add(piece.bottomLeftDiagonal())
directions.add(piece.bottomRightDiagonal())
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.topSide())
directions.add(piece.bottomSide())
directions.add(piece.rightSide())
directions.add(piece.leftSide())
directions.add(piece.color.topSide())
directions.add(piece.color.bottomSide())
directions.add(piece.color.rightSide())
directions.add(piece.color.leftSide())
for direction in directions:
# Slide in this direction as long as it's possible
var
@ -643,31 +744,39 @@ proc generateKingMoves(self: ChessBoard, location: Location): seq[Move] =
var
piece = self.grid[location.row, location.col]
doAssert piece.kind == King, &"generateKingMoves called on a {piece.kind}"
var directions: seq[Location] = @[piece.topLeftDiagonal(),
piece.topRightDiagonal(),
piece.bottomRightDiagonal(),
piece.bottomLeftDiagonal(),
piece.topSide(),
piece.bottomSide(),
piece.leftSide(),
piece.rightSide()]
var directions: seq[Location] = @[piece.color.topLeftDiagonal(),
piece.color.topRightDiagonal(),
piece.color.bottomRightDiagonal(),
piece.color.bottomLeftDiagonal(),
piece.color.topSide(),
piece.color.bottomSide(),
piece.color.leftSide(),
piece.color.rightSide()]
# Castling
let canCastle = self.canCastle()
if canCastle.queen:
directions.add(piece.color.longCastleKing())
if canCastle.king:
directions.add(piece.color.shortCastleKing())
var flag = Default
for direction in directions:
if direction == piece.color.longCastleKing():
flag = CastleLong
elif direction == piece.color.shortCastleKing():
flag = CastleShort
# Step in this direction once
let square: Location = location + direction
# End of board reached
if not square.isValid():
continue
let otherPiece = self.grid[square.row, square.col]
# A friendly piece is in the way
# A friendly piece is in the way, move onto the next
# direction
if otherPiece.color == piece.color:
continue
if otherPiece.color == piece.color.opposite:
# Target square contains an enemy piece: capture
# it
result.add(Move(startSquare: location, targetSquare: square, piece: piece))
continue
# Target square is empty
result.add(Move(startSquare: location, targetSquare: square, piece: piece))
# Target square is empty or contains an enemy piece:
# All good for us!
result.add(Move(startSquare: location, targetSquare: square, piece: piece, flag: flag))
proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] =
@ -675,14 +784,14 @@ proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] =
var
piece = self.grid[location.row, location.col]
doAssert piece.kind == Knight, &"generateKnightMoves called on a {piece.kind}"
var directions: seq[Location] = @[piece.bottomLeftKnightMove(),
piece.bottomRightKnightMove(),
piece.topLeftKnightMove(),
piece.topRightKnightMove(),
piece.bottomLeftKnightMove(long=false),
piece.bottomRightKnightMove(long=false),
piece.topLeftKnightMove(long=false),
piece.topRightKnightMove(long=false)]
var directions: seq[Location] = @[piece.color.bottomLeftKnightMove(),
piece.color.bottomRightKnightMove(),
piece.color.topLeftKnightMove(),
piece.color.topRightKnightMove(),
piece.color.bottomLeftKnightMove(long=false),
piece.color.bottomRightKnightMove(long=false),
piece.color.topLeftKnightMove(long=false),
piece.color.topRightKnightMove(long=false)]
for direction in directions:
# Jump to this square
let square: Location = location + direction
@ -703,7 +812,7 @@ proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] =
proc generateMoves(self: ChessBoard, location: Location): seq[Move] =
## Returns the list of possible legal chess moves for the
## Returns the list of possible pseudo-legal chess moves for the
## piece in the given location
let piece = self.grid[location.row, location.col]
case piece.kind:
@ -719,41 +828,29 @@ proc generateMoves(self: ChessBoard, location: Location): seq[Move] =
return @[]
proc getAttackers*(self: ChessBoard, square: string): seq[Piece] =
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 getAttackers*(self: ChessBoard, square: Location): seq[Piece] =
## Returns all the attackers of the given square
let loc = square.algebraicToLocation()
for move in self.position.attacked.black:
if move.targetSquare == loc:
if move.targetSquare == square:
result.add(move.piece)
for move in self.position.attacked.white:
if move.targetSquare == loc:
if move.targetSquare == square:
result.add(move.piece)
proc getAttackersFor*(self: ChessBoard, square: string, color: PieceColor): seq[Piece] =
## Returns all the attackers of the given square
## for the given color
let loc = square.algebraicToLocation()
case color:
of White:
for move in self.position.attacked.black:
if move.targetSquare == loc:
result.add(move.piece)
of Black:
for move in self.position.attacked.white:
if move.targetSquare == loc:
result.add(move.piece)
else:
discard
# We don't use getAttackers because this one only cares about whether
# the square is attacked or not (and can therefore exit earlier than
# getAttackers)
proc isAttacked*(self: ChessBoard, loc: Location): bool =
## Returns whether the given location is attacked
## by the opponent. If the location is empty, this
## function returns true regardless of which color
## the attackers are
## by the current opponent
let piece = self.grid[loc.row, loc.col]
case piece.color:
of White:
@ -778,6 +875,7 @@ proc isAttacked*(self: ChessBoard, loc: Location): bool =
discard
proc isAttacked*(self: ChessBoard, square: string): bool =
## Returns whether the given square is attacked
## by its opponent
@ -805,6 +903,10 @@ proc updateAttackedSquares(self: ChessBoard) =
for loc in self.position.pieces.white.bishops:
for move in self.generateMoves(loc):
self.position.attacked.white.add(move)
# Knights
for loc in self.position.pieces.white.knights:
for move in self.generateMoves(loc):
self.position.attacked.white.add(move)
# Rooks
for loc in self.position.pieces.white.rooks:
for move in self.generateMoves(loc):
@ -824,6 +926,9 @@ proc updateAttackedSquares(self: ChessBoard) =
for loc in self.position.pieces.black.bishops:
for move in self.generateMoves(loc):
self.position.attacked.black.add(move)
for loc in self.position.pieces.black.knights:
for move in self.generateMoves(loc):
self.position.attacked.white.add(move)
for loc in self.position.pieces.black.rooks:
for move in self.generateMoves(loc):
self.position.attacked.black.add(move)
@ -898,12 +1003,7 @@ proc movePiece(self: ChessBoard, move: Move) =
self.position.pieces.white.rooks.delete(self.position.pieces.white.rooks.find(move.startSquare))
self.position.pieces.white.rooks.add(move.targetSquare)
of Queen:
try:
self.position.pieces.white.queens.delete(self.position.pieces.white.queens.find(move.startSquare))
except:
echo self.position.pieces.white.queens
echo move.startSquare
raise getCurrentException()
self.position.pieces.white.queens.delete(self.position.pieces.white.queens.find(move.startSquare))
self.position.pieces.white.queens.add(move.targetSquare)
of King:
self.position.pieces.white.king = move.targetSquare
@ -944,31 +1044,13 @@ proc updatePositions(self: ChessBoard, move: Move) =
let capture = self.getCapture(move)
if capture != emptyLocation():
self.position.captured = self.grid[capture.row, capture.col]
if capture != self.getEnPassantTarget():
# En passant is handled elsewhere
self.removePiece(capture)
# Update the positional metadata of the moving piece
self.movePiece(move)
proc inCheck*(self: ChessBoard, color: PieceColor = None): bool =
## Returns whether the given color's
## king is in check. If the color is
## set to None, checks are checked
## for the active color's king
case color:
of White:
return self.isAttacked(self.position.pieces.white.king)
of Black:
return self.isAttacked(self.position.pieces.black.king)
of None:
case self.getActiveColor():
of White:
return self.isAttacked(self.position.pieces.white.king)
of Black:
return self.isAttacked(self.position.pieces.black.king)
else:
# Unreachable
discard
proc doMove(self: ChessBoard, move: Move) =
## Internal function called by makeMove after
## performing legality checks on the given move. Can
@ -977,25 +1059,74 @@ proc doMove(self: ChessBoard, move: Move) =
# Final checks
# Record the move in the position
self.position.move = move
# Needed to detect draw by the 50 move rule
var
halfMoveClock = self.position.halfMoveClock
fullMoveCount = self.position.fullMoveCount
castlingAvailable = self.position.castlingAvailable
if move.piece.kind == Pawn or self.isCapture(move):
halfMoveClock = 0
else:
inc(halfMoveClock)
if move.piece.color == Black:
inc(fullMoveCount)
# TODO: Castling
# Record the move in the position
self.position.move = move
# 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 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:
location = move.piece.color.queenSideRook()
target = move.piece.color.longCastleRook()
let rook = self.grid[location.row, location.col]
let move = Move(startSquare: location, targetSquare: location + target, piece: rook, flag: move.flag)
self.updatePositions(move)
self.updateAttackedSquares()
# Record final position for future reference
self.positions.add(self.position)
# Create new position with
@ -1004,19 +1135,19 @@ proc doMove(self: ChessBoard, move: Move) =
fullMoveCount: fullMoveCount,
captured: emptyPiece(),
turn: self.getActiveColor().opposite,
# Inherit values from current position
# (they are already up to date by this point)
castling: self.position.castling,
attacked: self.position.attacked,
castlingAvailable: castlingAvailable,
# Updated at the next call to doMove()
move: emptyMove(),
# Inherit values from current position
# (they are updated later anyway)
pieces: self.position.pieces,
attacked: self.position.attacked,
)
# Check for double pawn push
if move.piece.kind == Pawn and abs(move.startSquare.row - move.targetSquare.row) == 2:
newPos.enPassantSquare = Move(piece: move.piece,
startSquare: (move.startSquare.row, move.startSquare.col),
targetSquare: move.targetSquare + move.piece.bottomSide())
targetSquare: move.targetSquare + move.piece.color.bottomSide())
else:
newPos.enPassantSquare = emptyMove()
@ -1029,7 +1160,6 @@ proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) =
## was previously located there: use with caution. Does
## not automatically update the attacked square metadata
## or other positional information
doAssert piece.kind != King, "spawnPiece: cannot spawn a king"
case piece.color:
of White:
case piece.kind:
@ -1043,6 +1173,8 @@ proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) =
self.position.pieces.white.rooks.add(location)
of Queen:
self.position.pieces.white.queens.add(location)
of King:
self.position.pieces.white.king = location
else:
discard
of Black:
@ -1057,6 +1189,8 @@ proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) =
self.position.pieces.black.rooks.add(location)
of Queen:
self.position.pieces.black.queens.add(location)
of King:
self.position.pieces.black.king = location
else:
discard
else:
@ -1089,7 +1223,8 @@ proc undoLastMove*(self: ChessBoard): Move {.discardable.} =
proc isLegal(self: ChessBoard, move: Move): bool =
## Returns whether the given move is legal
var move = move
# Start square doesn't contain a piece (and it isn't the en passant square)
# or it's not this player's turn to move
if (move.piece.kind == Empty and move.targetSquare != self.getEnPassantTarget()) or move.piece.color != self.getActiveColor():
@ -1098,12 +1233,24 @@ proc isLegal(self: ChessBoard, move: Move): bool =
# Destination square is occupied by a friendly piece
if destination.kind != Empty and destination.color == self.getActiveColor():
return false
echo "A"
echo move.piece.color.longCastleKing()
echo move.startSquare
echo move.targetSquare
echo "B"
if move.piece.kind == King and move.piece.color.longCastleKing() + move.startSquare == move.targetSquare:
move.flag = CastleLong
elif move.piece.kind == King and move.piece.color.shortCastleKing() + move.startSquare == move.targetSquare:
move.flag = CastleShort
echo move
echo self.generateMoves(move.startSquare)
if move notin self.generateMoves(move.startSquare):
# Piece cannot arrive to destination (blocked,
# pinned or otherwise invalid move)
# Piece cannot arrive to destination (blocked
# or otherwise invalid move)
return false
self.doMove(move)
defer: self.undoLastMove()
defer: discard self.undoLastMove()
# Move would reveal an attack
# on our king: not allowed
if self.inCheck(move.piece.color):
@ -1114,6 +1261,7 @@ proc isLegal(self: ChessBoard, move: Move): bool =
proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} =
## Like the other makeMove(), but with a Move object
result = move
if not self.isLegal(move):
return emptyMove()

View File

@ -8,16 +8,20 @@ import std/strutils
when isMainModule:
setControlCHook(proc () {.noconv.} = echo ""; quit(0))
const fen = "rnbqkbnr/2p/8/8/8/8/P7/RNBQKBNR w KQkq - 0 1"
var
board = newChessboardFromFEN("rnbqkbnr/2p/8/8/8/8/P7/RNBQKBNR w KQkq - 0 1")
board = newChessboardFromFEN(fen)
canCastle: tuple[queen, king: bool]
data: string
move: Move
echo "\x1Bc"
while true:
canCastle = board.canCastle()
echo &"{board.pretty()}"
echo &"Turn: {board.getActiveColor()}"
echo &"Moves: {board.getMoveCount()} full, {board.getHalfMoveCount()} half"
echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
stdout.write(&"En passant target: ")
if board.getEnPassantTarget() != emptyLocation():
echo board.getEnPassantTarget().locationToAlgebraic()
@ -28,7 +32,7 @@ when isMainModule:
echo &"Yes"
else:
echo "No"
stdout.write("\nMove -> ")
stdout.write("\nMove(s) -> ")
try:
data = readLine(stdin).strip(chars={'\0', ' '})
except IOError:
@ -37,14 +41,20 @@ when isMainModule:
if data == "undo":
echo &"\x1BcUndo: {board.undoLastMove()}"
continue
if len(data) != 4:
echo "\x1BcError: invalid move"
if data == "reset":
echo &"\x1BcBoard reset"
board = newChessboardFromFEN(fen)
continue
try:
move = board.makeMove(data[0..1], data[2..3])
except ValueError:
echo &"\x1BcError: {getCurrentExceptionMsg()}"
if move == emptyMove():
echo &"\x1BcError: move is illegal"
else:
echo "\x1Bc"
for moveChars in data.split(" "):
if len(moveChars) != 4:
echo "\x1BcError: invalid move"
break
try:
move = board.makeMove(moveChars[0..1], moveChars[2..3])
except ValueError:
echo &"\x1BcError: {getCurrentExceptionMsg()}"
if move == emptyMove():
echo &"\x1BcError: move '{moveChars}' is illegal"
break
else:
echo "\x1Bc"