Initial work on the move list

This commit is contained in:
Mattia Giambirtone 2023-10-17 15:08:46 +02:00
parent ca498ebc42
commit 79477fe077
2 changed files with 312 additions and 178 deletions

View File

@ -21,11 +21,12 @@ import std/strformat
type
# Useful type aliases
Location = tuple[row, col: int]
Location* = tuple[row, col: int]
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
@ -65,11 +66,15 @@ type
targetSquare*: Location
flag*: MoveFlag
ChessBoard* = ref object
## A chess board object
grid: Matrix[Piece]
# Currently active color
turn: PieceColor
Position* = ref object
## A chess position
move: Move
# Stores castling metadata
castling: Castling
# Number of half-moves that were performed
# to reach this position starting from the
# root of the tree
plyFromRoot: int
# Number of half moves since
# last piece capture or pawn movement.
# Used for the 50-move rule
@ -77,8 +82,6 @@ type
# Full move counter. Increments
# every 2 ply
fullMoveCount: int
# Stores metadata for castling.
castling: tuple[white, black: tuple[queen, king: bool]]
# 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
@ -87,6 +90,19 @@ type
pieces: tuple[white: Pieces, black: Pieces]
# Potential attacking moves for black and white
attacked: tuple[white: seq[Move], black: seq[Move]]
# Has any piece been captured to reach this position?
captured: Piece
# Active color
turn: PieceColor
ChessBoard* = ref object
## A chess board object
grid: Matrix[Piece]
position: Position
positionIndex: int
# List of reached positions
positions: seq[Position]
# Initialized only once, copied every time
@ -177,7 +193,19 @@ func topRightKnightMove(piece: Piece, long: bool = true): Location {.inline.} =
proc getActiveColor*(self: ChessBoard): PieceColor =
## Returns the currently active color
## (turn of who has to move)
return self.turn
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
func getStartRow(piece: Piece): int {.inline.} =
@ -218,9 +246,12 @@ proc newChessboard: ChessBoard =
new(result)
# Turns our flat sequence into an 8x8 grid
result.grid = newMatrixFromSeq[Piece](empty, (8, 8))
result.attacked = (@[], @[])
result.enPassantSquare = emptyMove()
result.turn = White
result.position = Position(attacked: (@[], @[]),
enPassantSquare: emptyMove(),
move: emptyMove(),
turn: White)
result.positionIndex = 0
proc newChessboardFromFEN*(state: string): ChessBoard =
@ -261,33 +292,33 @@ proc newChessboardFromFEN*(state: string): ChessBoard =
of Black:
case piece.kind:
of Pawn:
result.pieces.black.pawns.add((row, column))
result.position.pieces.black.pawns.add((row, column))
of Bishop:
result.pieces.black.bishops.add((row, column))
result.position.pieces.black.bishops.add((row, column))
of Knight:
result.pieces.black.knights.add((row, column))
result.position.pieces.black.knights.add((row, column))
of Rook:
result.pieces.black.rooks.add((row, column))
result.position.pieces.black.rooks.add((row, column))
of Queen:
result.pieces.black.queens.add((row, column))
result.position.pieces.black.queens.add((row, column))
of King:
result.pieces.black.king = (row, column)
result.position.pieces.black.king = (row, column)
else:
discard
of White:
case piece.kind:
of Pawn:
result.pieces.white.pawns.add((row, column))
result.position.pieces.white.pawns.add((row, column))
of Bishop:
result.pieces.white.bishops.add((row, column))
result.position.pieces.white.bishops.add((row, column))
of Knight:
result.pieces.white.knights.add((row, column))
result.position.pieces.white.knights.add((row, column))
of Rook:
result.pieces.white.rooks.add((row, column))
result.position.pieces.white.rooks.add((row, column))
of Queen:
result.pieces.white.queens.add((row, column))
result.position.pieces.white.queens.add((row, column))
of King:
result.pieces.white.king = (row, column)
result.position.pieces.white.king = (row, column)
else:
discard
else:
@ -310,9 +341,9 @@ proc newChessboardFromFEN*(state: string): ChessBoard =
# Active color
case c:
of 'w':
result.turn = White
result.position.turn = White
of 'b':
result.turn = Black
result.position.turn = Black
else:
raise newException(ValueError, "invalid active color identifier in FEN string")
of 2:
@ -324,13 +355,13 @@ proc newChessboardFromFEN*(state: string): ChessBoard =
# by default
discard
of 'K':
result.castling.white.king = true
result.position.castling.white.king = true
of 'Q':
result.castling.white.queen = true
result.position.castling.white.queen = true
of 'k':
result.castling.black.king = true
result.position.castling.black.king = true
of 'q':
result.castling.black.queen = true
result.position.castling.black.queen = true
else:
raise newException(ValueError, "invalid castling availability in FEN string")
of 3:
@ -340,11 +371,11 @@ proc newChessboardFromFEN*(state: string): ChessBoard =
# Field is already uninitialized to the correct state
discard
else:
result.enPassantSquare.targetSquare = state[index..index+1].algebraicToPosition()
# Just for cleanliness purposes, we fill in the other positional metadata as
result.position.enPassantSquare.targetSquare = state[index..index+1].algebraicToPosition()
# Just for cleanliness purposes, we fill in the other metadata as
# well
result.enPassantSquare.piece.color = if result.turn == Black: White else: Black
result.enPassantSquare.piece.kind = Pawn
result.position.enPassantSquare.piece.color = result.getActiveColor()
result.position.enPassantSquare.piece.kind = Pawn
# Square metadata is 2 bytes long
inc(index)
of 4:
@ -356,14 +387,14 @@ proc newChessboardFromFEN*(state: string): ChessBoard =
# Backtrack so the space is seen by the
# next iteration of the loop
dec(index)
result.halfMoveClock = parseInt(s)
result.position.halfMoveClock = parseInt(s)
of 5:
# Fullmove number
var s = ""
while index <= state.high():
s.add(state[index])
inc(index)
result.fullMoveCount = parseInt(s)
result.position.fullMoveCount = parseInt(s)
else:
raise newException(ValueError, "too many fields in FEN string")
inc(index)
@ -382,15 +413,15 @@ proc countPieces*(self: ChessBoard, kind: PieceKind, color: PieceColor): int =
of White:
case kind:
of Pawn:
return self.pieces.white.pawns.len()
return self.position.pieces.white.pawns.len()
of Bishop:
return self.pieces.white.bishops.len()
return self.position.pieces.white.bishops.len()
of Knight:
return self.pieces.white.knights.len()
return self.position.pieces.white.knights.len()
of Rook:
return self.pieces.white.rooks.len()
return self.position.pieces.white.rooks.len()
of Queen:
return self.pieces.white.queens.len()
return self.position.pieces.white.queens.len()
of King:
# There shall be only one, forever
return 1
@ -399,15 +430,15 @@ proc countPieces*(self: ChessBoard, kind: PieceKind, color: PieceColor): int =
of Black:
case kind:
of Pawn:
return self.pieces.black.pawns.len()
return self.position.pieces.black.pawns.len()
of Bishop:
return self.pieces.black.bishops.len()
return self.position.pieces.black.bishops.len()
of Knight:
return self.pieces.black.knights.len()
return self.position.pieces.black.knights.len()
of Rook:
return self.pieces.black.rooks.len()
return self.position.pieces.black.rooks.len()
of Queen:
return self.pieces.black.queens.len()
return self.position.pieces.black.queens.len()
of King:
# In perpetuity
return 1
@ -468,7 +499,7 @@ proc getCapture*(self: ChessBoard, move: Move): Location =
result = emptyLocation()
let target = self.grid[move.targetSquare.row, move.targetSquare.col]
if target.color == None:
if move.targetSquare != self.enPassantSquare.targetSquare:
if move.targetSquare != self.position.enPassantSquare.targetSquare:
return
else:
return ((if move.piece.color == White: move.targetSquare.row + 1 else: move.targetSquare.row - 1), move.targetSquare.col)
@ -497,10 +528,10 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
# If the pawn is on its first rank, it can push two squares
if location.row == piece.getStartRow():
locations.add(piece.doublePush())
if self.enPassantSquare.piece.color == piece.color.opposite:
if abs(self.enPassantSquare.targetSquare.col - location.col) == 1 and abs(self.enPassantSquare.targetSquare.row - location.row) == 1:
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
locations.add(self.enPassantSquare.targetSquare)
locations.add(self.position.enPassantSquare.targetSquare)
# They can also move on either diagonal one
# square, but only to capture
if location.col in 1..6:
@ -665,10 +696,10 @@ proc generateMoves(self: ChessBoard, location: Location): seq[Move] =
proc getAttackers*(self: ChessBoard, square: string): seq[Piece] =
## Returns all the attackers of the given square
let loc = square.algebraicToPosition()
for move in self.attacked.black:
for move in self.position.attacked.black:
if move.targetSquare == loc:
result.add(move.piece)
for move in self.attacked.white:
for move in self.position.attacked.white:
if move.targetSquare == loc:
result.add(move.piece)
@ -679,11 +710,11 @@ proc getAttackersFor*(self: ChessBoard, square: string, color: PieceColor): seq[
let loc = square.algebraicToPosition()
case color:
of White:
for move in self.attacked.black:
for move in self.position.attacked.black:
if move.targetSquare == loc:
result.add(move.piece)
of Black:
for move in self.attacked.white:
for move in self.position.attacked.white:
if move.targetSquare == loc:
result.add(move.piece)
else:
@ -700,21 +731,21 @@ proc isAttacked*(self: ChessBoard, loc: Location): bool =
let piece = self.grid[loc.row, loc.col]
case piece.color:
of White:
for move in self.attacked.black:
for move in self.position.attacked.black:
if move.targetSquare == loc:
return true
of Black:
for move in self.attacked.white:
for move in self.position.attacked.white:
if move.targetSquare == loc:
return true
of None:
case self.turn:
case self.getActiveColor():
of White:
for move in self.attacked.black:
for move in self.position.attacked.black:
if move.targetSquare == loc:
return true
of Black:
for move in self.attacked.white:
for move in self.position.attacked.white:
if move.targetSquare == loc:
return true
else:
@ -735,46 +766,46 @@ proc updateAttackedSquares(self: ChessBoard) =
# 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.attacked.white.setLen(0)
self.attacked.black.setLen(0)
self.position.attacked.white.setLen(0)
self.position.attacked.black.setLen(0)
# Go over each piece one by one and see which squares
# it currently attacks
# Pawns
for loc in self.pieces.white.pawns:
for loc in self.position.pieces.white.pawns:
for move in self.generateMoves(loc):
self.attacked.white.add(move)
self.position.attacked.white.add(move)
# Bishops
for loc in self.pieces.white.bishops:
for loc in self.position.pieces.white.bishops:
for move in self.generateMoves(loc):
self.attacked.white.add(move)
# rooks
for loc in self.pieces.white.rooks:
self.position.attacked.white.add(move)
# Rooks
for loc in self.position.pieces.white.rooks:
for move in self.generateMoves(loc):
self.attacked.white.add(move)
self.position.attacked.white.add(move)
# Queens
for loc in self.pieces.white.queens:
for loc in self.position.pieces.white.queens:
for move in self.generateMoves(loc):
self.attacked.white.add(move)
self.position.attacked.white.add(move)
# King
for move in self.generateMoves(self.pieces.white.king):
self.attacked.white.add(move)
for move in self.generateMoves(self.position.pieces.white.king):
self.position.attacked.white.add(move)
# Same for black
for loc in self.pieces.black.pawns:
for loc in self.position.pieces.black.pawns:
for move in self.generateMoves(loc):
self.attacked.black.add(move)
for loc in self.pieces.black.bishops:
self.position.attacked.black.add(move)
for loc in self.position.pieces.black.bishops:
for move in self.generateMoves(loc):
self.attacked.black.add(move)
for loc in self.pieces.black.rooks:
self.position.attacked.black.add(move)
for loc in self.position.pieces.black.rooks:
for move in self.generateMoves(loc):
self.attacked.black.add(move)
for loc in self.pieces.black.queens:
self.position.attacked.black.add(move)
for loc in self.position.pieces.black.queens:
for move in self.generateMoves(loc):
self.attacked.black.add(move)
for move in self.generateMoves(self.pieces.black.king):
self.attacked.black.add(move)
self.position.attacked.black.add(move)
for move in self.generateMoves(self.position.pieces.black.king):
self.position.attacked.black.add(move)
proc removePiece(self: ChessBoard, location: Location) =
@ -786,15 +817,15 @@ proc removePiece(self: ChessBoard, location: Location) =
of White:
case piece.kind:
of Pawn:
self.pieces.white.pawns.delete(self.pieces.white.pawns.find(location))
self.position.pieces.white.pawns.delete(self.position.pieces.white.pawns.find(location))
of Bishop:
self.pieces.white.pawns.delete(self.pieces.white.bishops.find(location))
self.position.pieces.white.pawns.delete(self.position.pieces.white.bishops.find(location))
of Knight:
self.pieces.white.pawns.delete(self.pieces.white.knights.find(location))
self.position.pieces.white.pawns.delete(self.position.pieces.white.knights.find(location))
of Rook:
self.pieces.white.rooks.delete(self.pieces.white.rooks.find(location))
self.position.pieces.white.rooks.delete(self.position.pieces.white.rooks.find(location))
of Queen:
self.pieces.white.queens.delete(self.pieces.white.queens.find(location))
self.position.pieces.white.queens.delete(self.position.pieces.white.queens.find(location))
of King:
doAssert false, "removePiece: attempted to remove the white king"
else:
@ -802,15 +833,15 @@ proc removePiece(self: ChessBoard, location: Location) =
of Black:
case piece.kind:
of Pawn:
self.pieces.black.pawns.delete(self.pieces.white.pawns.find(location))
self.position.pieces.black.pawns.delete(self.position.pieces.white.pawns.find(location))
of Bishop:
self.pieces.black.bishops.delete(self.pieces.black.bishops.find(location))
self.position.pieces.black.bishops.delete(self.position.pieces.black.bishops.find(location))
of Knight:
self.pieces.black.knights.delete(self.pieces.black.knights.find(location))
self.position.pieces.black.knights.delete(self.position.pieces.black.knights.find(location))
of Rook:
self.pieces.black.rooks.delete(self.pieces.black.rooks.find(location))
self.position.pieces.black.rooks.delete(self.position.pieces.black.rooks.find(location))
of Queen:
self.pieces.black.queens.delete(self.pieces.black.queens.find(location))
self.position.pieces.black.queens.delete(self.position.pieces.black.queens.find(location))
of King:
doAssert false, "removePiece: attempted to remove the black king"
else:
@ -819,6 +850,76 @@ proc removePiece(self: ChessBoard, location: Location) =
discard
proc handleCapture(self: ChessBoard, move: Move) =
## Handles capturing (assumes the move is valid)
let targetPiece = self.grid[move.targetSquare.row, move.targetSquare.col]
assert self.position.captured == emptyPiece(), "capture: last capture is non-empty"
self.position.captured = move.piece
self.removePiece(move.targetSquare)
proc movePiece(self: ChessBoard, move: Move) =
## Internal helper to move a piece. Does
## not update attacked squares, just position
## metadata and the grid itself
case move.piece.color:
of White:
case move.piece.kind:
of Pawn:
# The way things are structured, we don't care about the order
# of this list, so we can add and remove entries as we please
self.position.pieces.white.pawns.delete(self.position.pieces.white.pawns.find(move.startSquare))
self.position.pieces.white.pawns.add(move.targetSquare)
of Bishop:
self.position.pieces.white.bishops.delete(self.position.pieces.white.bishops.find(move.startSquare))
self.position.pieces.white.bishops.add(move.targetSquare)
of Knight:
self.position.pieces.white.knights.delete(self.position.pieces.white.knights.find(move.startSquare))
self.position.pieces.white.knights.add(move.targetSquare)
of Rook:
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.add(move.targetSquare)
of King:
self.position.pieces.white.king = move.targetSquare
else:
discard
of Black:
case move.piece.kind:
of Pawn:
self.position.pieces.black.pawns.delete(self.position.pieces.black.pawns.find(move.startSquare))
self.position.pieces.black.pawns.add(move.targetSquare)
of Bishop:
self.position.pieces.black.bishops.delete(self.position.pieces.black.bishops.find(move.startSquare))
self.position.pieces.black.bishops.add(move.targetSquare)
of Knight:
self.position.pieces.black.knights.delete(self.position.pieces.black.knights.find(move.startSquare))
self.position.pieces.black.knights.add(move.targetSquare)
of Rook:
self.position.pieces.black.rooks.delete(self.position.pieces.black.rooks.find(move.startSquare))
self.position.pieces.black.rooks.add(move.targetSquare)
of Queen:
self.position.pieces.black.queens.delete(self.position.pieces.black.queens.find(move.startSquare))
self.position.pieces.black.queens.add(move.targetSquare)
of King:
self.position.pieces.black.king = move.targetSquare
else:
discard
else:
discard
# Empty out the starting square
self.grid[move.startSquare.row, move.startSquare.col] = emptyPiece()
# Actually move the piece
self.grid[move.targetSquare.row, move.targetSquare.col] = move.piece
proc updatePositions(self: ChessBoard, move: Move) =
## Internal helper to update the position of
## the pieces on the board after a move
@ -828,59 +929,9 @@ proc updatePositions(self: ChessBoard, move: Move) =
# of doing it ourselves because there's a bunch of metadata that needs
# to be updated to do this properly and I thought it'd fit into its neat
# little function
self.removePiece(capture)
self.handleCapture(move)
# Update the positional metadata of the moving piece
case move.piece.color:
of White:
case move.piece.kind:
of Pawn:
# The way things are structured, we don't care about the order
# of this list, so we can add and remove entries as we please
self.pieces.white.pawns.delete(self.pieces.white.pawns.find(move.startSquare))
self.pieces.white.pawns.add(move.targetSquare)
of Bishop:
self.pieces.white.bishops.delete(self.pieces.white.bishops.find(move.startSquare))
self.pieces.white.bishops.add(move.targetSquare)
of Knight:
self.pieces.white.knights.delete(self.pieces.white.knights.find(move.startSquare))
self.pieces.white.knights.add(move.targetSquare)
of Rook:
self.pieces.white.rooks.delete(self.pieces.white.rooks.find(move.startSquare))
self.pieces.white.rooks.add(move.targetSquare)
of Queen:
self.pieces.white.queens.delete(self.pieces.white.queens.find(move.startSquare))
self.pieces.white.queens.add(move.targetSquare)
of King:
self.pieces.white.king = move.targetSquare
else:
discard
of Black:
case move.piece.kind:
of Pawn:
self.pieces.black.pawns.delete(self.pieces.black.pawns.find(move.startSquare))
self.pieces.black.pawns.add(move.targetSquare)
of Bishop:
self.pieces.black.bishops.delete(self.pieces.black.bishops.find(move.startSquare))
self.pieces.black.bishops.add(move.targetSquare)
of Knight:
self.pieces.black.knights.delete(self.pieces.black.knights.find(move.startSquare))
self.pieces.black.knights.add(move.targetSquare)
of Rook:
self.pieces.black.rooks.delete(self.pieces.black.rooks.find(move.startSquare))
self.pieces.black.rooks.add(move.targetSquare)
of Queen:
self.pieces.black.queens.delete(self.pieces.black.queens.find(move.startSquare))
self.pieces.black.queens.add(move.targetSquare)
of King:
self.pieces.black.king = move.targetSquare
else:
discard
else:
discard
# Empty out the starting square
self.grid[move.startSquare.row, move.startSquare.col] = emptyPiece()
# Actually move the piece
self.grid[move.targetSquare.row, move.targetSquare.col] = move.piece
self.movePiece(move)
proc inCheck*(self: ChessBoard, color: PieceColor = None): bool =
@ -890,15 +941,15 @@ proc inCheck*(self: ChessBoard, color: PieceColor = None): bool =
## for the active color's king
case color:
of White:
return self.isAttacked(self.pieces.white.king)
return self.isAttacked(self.position.pieces.white.king)
of Black:
return self.isAttacked(self.pieces.black.king)
return self.isAttacked(self.position.pieces.black.king)
of None:
case self.turn:
case self.getActiveColor():
of White:
return self.isAttacked(self.pieces.white.king)
return self.isAttacked(self.position.pieces.white.king)
of Black:
return self.isAttacked(self.pieces.black.king)
return self.isAttacked(self.position.pieces.black.king)
else:
# Unreachable
discard
@ -909,46 +960,128 @@ proc doMove(self: ChessBoard, move: Move) =
## performing legality checks on the given move. Can
## be used in performance-critical paths where
## a move is already known to be legal
self.updatePositions(move)
self.updateAttackedSquares()
# Final checks
# Needed to detect draw by the 50 move rule
if move.piece.kind != Pawn and not self.isCapture(move):
inc(self.position.halfMoveClock)
else:
self.position.halfMoveClock = 0
if (self.position.halfMoveClock and 1) == 0: # Equivalent to (x mod 2) == 0, just much faster
inc(self.position.fullMoveCount)
# En passant is possible only immediately after the
# pawn has moved
if self.enPassantSquare != emptyMove() and self.enPassantSquare.piece.color == self.turn.opposite():
self.enPassantSquare = emptyMove()
self.turn = self.turn.opposite()
if self.position.enPassantSquare != emptyMove() and self.position.enPassantSquare.piece.color == self.getActiveColor().opposite():
self.position.enPassantSquare = emptyMove()
# TODO: Castling
self.position.move = move
# Update position and attack metadata
self.updatePositions(move)
self.updateAttackedSquares()
# Record final position for future reference
self.positions.add(self.position)
# Create new position with
var newPos = Position(plyFromRoot: self.position.plyFromRoot + 1,
captured: emptyPiece(),
turn: self.position.turn.opposite(),
# Inherit values from current position
# (they are already up to date by this point)
castling: self.position.castling,
enPassantSquare: self.position.enPassantSquare,
attacked: (@[], @[])
)
self.position = newPos
inc(self.positionIndex)
proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) =
## Internal helper to "spawn" a given piece at the given
## location. Note that this will overwrite whatever 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:
of Pawn:
self.position.pieces.white.pawns.add(location)
of Knight:
self.position.pieces.white.knights.add(location)
of Bishop:
self.position.pieces.white.bishops.add(location)
of Rook:
self.position.pieces.white.rooks.add(location)
of Queen:
self.position.pieces.white.queens.add(location)
else:
discard
of Black:
case piece.kind:
of Pawn:
self.position.pieces.black.pawns.add(location)
of Knight:
self.position.pieces.black.knights.add(location)
of Bishop:
self.position.pieces.black.bishops.add(location)
of Rook:
self.position.pieces.black.rooks.add(location)
of Queen:
self.position.pieces.black.queens.add(location)
else:
discard
else:
# Unreachable
discard
self.grid[location.row, location.col] = piece
proc undoMove(self: ChessBoard, move: Move): Move {.discardable.} =
## Undoes the given move if possible
result = Move(piece: move.piece, startSquare: move.targetSquare, targetSquare: move.startSquare)
let castling = self.castling
let enPassant = self.enPassantSquare
# Swap start and target square and do the move in reverse
self.doMove(result)
# We need to reset the entire position when the move is undone!
self.enPassantSquare = enPassant
#self.turn = self.turn.opposite()
self.castling = castling
proc undoLastMove*(self: ChessBoard): Move {.discardable.} =
## Undoes the last move, restoring any captured pieces,
## as well castling and en passant status. If there are
## no moves to undo, this is a no-op. Returns the move
## that was performed (which may be an empty move)
result = emptyMove()
if self.positions.len() == 0:
return
let positionIndex = max(0, self.positionIndex - 1)
if positionIndex in 0..self.positions.high():
self.positionIndex = positionIndex
self.position = self.positions[positionIndex]
let
currentMove = self.position.move
oppositeMove = Move(piece: currentMove.piece, targetSquare: currentMove.startSquare, startSquare: currentMove.targetSquare)
self.spawnPiece(currentMove.startSquare, currentMove.piece)
if self.position.captured != emptyPiece():
self.spawnPiece(self.position.move.targetSquare, self.position.captured)
self.updateAttackedSquares()
self.updatePositions(oppositeMove)
return self.position.move
proc checkMove(self: ChessBoard, move: Move): bool =
## Internal function called by makeMove to check a move for legality
# Start square doesn't contain a piece (and it isn't the en passant square)
# or it is of the wrong color for which turn it is to move
if move.piece.kind == Empty or move.piece.color != self.turn:
if move.piece.kind == Empty or move.piece.color != self.getActiveColor():
return false
var destination = self.grid[move.targetSquare.row, move.targetSquare.col]
# Destination square is occupied by a piece of the same color as the piece
# being moved: illegal!
if destination.kind != Empty and destination.color == self.turn:
if destination.kind != Empty and destination.color == self.getActiveColor():
return false
if move notin self.generateMoves(move.startSquare):
# Piece cannot arrive to destination (blocked,
# pinned, or otherwise invalid move)
return false
self.doMove(move)
defer: self.undoMove(move)
defer: self.undoLastMove()
# Move would reveal an attack
# on our king: not allowed
if self.inCheck(move.piece.color):
@ -962,11 +1095,6 @@ proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} =
result = move
if not self.checkMove(move):
return emptyMove()
# 50 move rule
if move.piece.kind != Pawn and not self.isCapture(move):
inc(self.halfMoveClock)
else:
self.halfMoveClock = 0
self.doMove(result)
@ -986,6 +1114,13 @@ proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.disc
return self.makeMove(result)
proc makeMove*(self: ChessBoard, startSquare, targetSquare: Location): Move {.discardable.} =
## Like the other makeMove(), but with two locations
result = Move(startSquare: startSquare, targetSquare: targetSquare, piece: self.grid[startSquare.row, startSquare.col])
return self.makeMove(result)
proc `$`*(self: ChessBoard): string =
result &= "- - - - - - - -"

View File

@ -10,8 +10,6 @@ when isMainModule:
setControlCHook(proc () {.noconv.} = echo ""; quit(0))
var
board = newChessboardFromFEN("rnbqkbnr/8/8/8/8/8/8/RNBQKBNR w KQkq - 0 1")
startSquare: string
targetSquare: string
data: string
move: Move
@ -27,12 +25,13 @@ when isMainModule:
except IOError:
echo ""
break
if data == "undo":
echo &"Undo: {board.undoLastMove()}"
continue
if len(data) != 4:
continue
startSquare = data[0..1]
targetSquare = data[2..3]
try:
move = board.makeMove(startSquare, targetSquare)
move = board.makeMove(data[0..1], data[2..3])
except ValueError:
echo &"Error: {getCurrentExceptionMsg()}"
if move == emptyMove():