More movegen bug fixes (close!)

This commit is contained in:
Mattia Giambirtone 2024-04-10 13:45:29 +02:00
parent 89a96eaf52
commit 54a6217bd3
1 changed files with 61 additions and 51 deletions

View File

@ -76,7 +76,7 @@ type
Position* = ref object Position* = ref object
## A chess position ## A chess position
# Did the rooks on either side/the king move? # Did the rooks on either side or the king move?
castlingAvailable: tuple[white, black: tuple[queen, king: bool]] castlingAvailable: tuple[white, black: tuple[queen, king: bool]]
# Number of half-moves that were performed # Number of half-moves that were performed
# to reach this position starting from the # to reach this position starting from the
@ -95,7 +95,7 @@ type
pieces: tuple[white: Pieces, black: Pieces] pieces: tuple[white: Pieces, black: Pieces]
# Squares attacked by both sides # Squares attacked by both sides
attacked: tuple[white: Attacked, black: Attacked] attacked: tuple[white: Attacked, black: Attacked]
# Pieces pinned by both sides # Pieces pinned by both sides (only absolute pins)
pinned: tuple[white: Attacked, black: Attacked] pinned: tuple[white: Attacked, black: Attacked]
# Active color # Active color
turn: PieceColor turn: PieceColor
@ -105,14 +105,14 @@ type
# The actual board where pieces live # The actual board where pieces live
# (flattened 8x8 matrix) # (flattened 8x8 matrix)
grid: seq[Piece] grid: array[64, Piece]
# The current position # The current position
position: Position position: Position
# List of all previously reached positions # List of all previously reached positions
positions: seq[Position] positions: seq[Position]
# A bunch of simple utility functions # A bunch of simple utility functions and forward declarations
func emptyPiece*: Piece {.inline.} = Piece(kind: Empty, color: None) func emptyPiece*: Piece {.inline.} = Piece(kind: Empty, color: None)
func emptyLocation*: Location {.inline.} = (-1 , -1) func emptyLocation*: Location {.inline.} = (-1 , -1)
@ -145,7 +145,7 @@ proc extend[T](self: var seq[T], other: openarray[T]) {.inline.} =
for x in other: for x in other:
self.add(x) self.add(x)
proc resetBoard*(self: ChessBoard) proc updateBoard*(self: ChessBoard)
# Due to our board layout, directions of movement are reversed for white and black, so # Due to our board layout, directions of movement are reversed for white and black, so
# we need these helpers to avoid going mad with integer tuples and minus signs everywhere # we need these helpers to avoid going mad with integer tuples and minus signs everywhere
@ -290,10 +290,8 @@ func getLastRow(color: PieceColor): int {.inline.} =
proc newChessboard: ChessBoard = proc newChessboard: ChessBoard =
## Returns a new, empty chessboard ## Returns a new, empty chessboard
new(result) new(result)
# Turns our flat sequence into an 8x8 grid for i in 0..63:
result.grid = newSeqOfCap[Piece](64) result.grid[i] = emptyPiece()
for _ in 0..63:
result.grid.add(emptyPiece())
result.position = Position(attacked: (@[], @[]), result.position = Position(attacked: (@[], @[]),
enPassantSquare: emptyLocation(), enPassantSquare: emptyLocation(),
turn: White, turn: White,
@ -313,10 +311,10 @@ proc newChessboard: ChessBoard =
func coordToIndex(row, col: int): int {.inline.} = (row * 8) + col func coordToIndex(row, col: int): int {.inline.} = (row * 8) + col
func `[]`(self: seq[Piece], row, column: Natural): Piece {.inline.} = self[coordToIndex(row, column)] func `[]`(self: array[64, Piece], row, column: Natural): Piece {.inline.} = self[coordToIndex(row, column)]
proc `[]=`(self: var seq[Piece], row, column: Natural, piece: Piece) {.inline.} = self[coordToIndex(row, column)] = piece proc `[]=`(self: var array[64, Piece], row, column: Natural, piece: Piece) {.inline.} = self[coordToIndex(row, column)] = piece
func `[]`(self: seq[Piece], loc: Location): Piece {.inline.} = self[loc.row, loc.col] func `[]`(self: array[64, Piece], loc: Location): Piece {.inline.} = self[loc.row, loc.col]
proc `[]=`(self: var seq[Piece], loc: Location, piece: Piece) {.inline.} = self[loc.row, loc.col] = piece proc `[]=`(self: var array[64, Piece], loc: Location, piece: Piece) {.inline.} = self[loc.row, loc.col] = piece
proc newChessboardFromFEN*(fen: string): ChessBoard = proc newChessboardFromFEN*(fen: string): ChessBoard =
@ -535,10 +533,10 @@ func rankToColumn(rank: int): int8 {.inline.} =
return indeces[rank - 1] return indeces[rank - 1]
func rowToFile(row: int): int {.inline.} = func rowToFile(row: int): int8 {.inline.} =
## Converts a row into our grid into ## Converts a row into our grid into
## a chess file ## a chess file
const indeces = [8, 7, 6, 5, 4, 3, 2, 1] const indeces: array[8, int8] = [8, 7, 6, 5, 4, 3, 2, 1]
return indeces[row] return indeces[row]
@ -641,7 +639,9 @@ func getFlags*(move: Move): seq[MoveFlag] =
result.add(Default) result.add(Default)
func getKing(self: ChessBoard, color: PieceColor): Location {.inline.} = func getKing(self: ChessBoard, color: PieceColor = None): Location {.inline.} =
## Returns the location of the king for the given
## color (if it is None, the active color is used)
var color = color var color = color
if color == None: if color == None:
color = self.getActiveColor() color = self.getActiveColor()
@ -803,7 +803,8 @@ proc getCheckResolutions(self: ChessBoard, color: PieceColor): seq[Location] =
let let
attacker = attackers[0] attacker = attackers[0]
attackerPiece = self.grid[attacker.row, attacker.col] attackerPiece = self.grid[attacker.row, attacker.col]
attack = self.getAttackFor(attacker, king)
var attack = self.getAttackFor(attacker, king)
# Capturing the piece resolves the check # Capturing the piece resolves the check
result.add(attacker) result.add(attacker)
# Blocking the attack is also a viable strategy # Blocking the attack is also a viable strategy
@ -940,9 +941,15 @@ proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
if otherPiece.color == piece.color: if otherPiece.color == piece.color:
break break
if checked and square notin resolutions: if checked and square notin resolutions:
# We don't break out of the loop because # We don't always break out of the loop because
# we might resolve the check later # we might resolve the check later
continue if otherPiece.color == None:
# We can still move in this direction, so maybe
# the check can be resolved later
continue
else:
# Our movement is blocked, switch to next direction
break
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
@ -950,7 +957,7 @@ proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
# Can't capture the king # Can't capture the king
result.add(Move(startSquare: location, targetSquare: square, flags: Capture.uint16)) result.add(Move(startSquare: location, targetSquare: square, flags: Capture.uint16))
break break
# Target square is empty # Target square is empty, keep going
result.add(Move(startSquare: location, targetSquare: square)) result.add(Move(startSquare: location, targetSquare: square))
@ -1174,11 +1181,11 @@ proc updatePawnAttacks(self: ChessBoard) =
# squares they can move to do not match the squares # squares they can move to do not match the squares
# they can capture on. Sneaky fucks) # they can capture on. Sneaky fucks)
self.addAttack((loc, loc + White.topRightDiagonal(), White.topRightDiagonal()), White) self.addAttack((loc, loc + White.topRightDiagonal(), White.topRightDiagonal()), White)
self.addAttack((loc, loc + White.topLeftDiagonal(), White.topRightDiagonal()), White) self.addAttack((loc, loc + White.topLeftDiagonal(), White.topLeftDiagonal()), White)
# We do the same thing for black # We do the same thing for black
for loc in self.position.pieces.black.pawns: for loc in self.position.pieces.black.pawns:
self.addAttack((loc, loc + Black.topRightDiagonal(), Black.topRightDiagonal()), Black) self.addAttack((loc, loc + Black.topRightDiagonal(), Black.topRightDiagonal()), Black)
self.addAttack((loc, loc + Black.topLeftDiagonal(), Black.topRightDiagonal()), Black) self.addAttack((loc, loc + Black.topLeftDiagonal(), Black.topLeftDiagonal()), Black)
proc updateKingAttacks(self: ChessBoard) = proc updateKingAttacks(self: ChessBoard) =
@ -1258,7 +1265,7 @@ proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked
# We found an enemy piece that is not # We found an enemy piece that is not
# the enemy king. We don't break out # the enemy king. We don't break out
# immediately because we first want # immediately because we first want
# to check if we've pinned a piece # to check if we've pinned it to the king
var var
otherSquare: Location = square otherSquare: Location = square
behindPiece: Piece behindPiece: Piece
@ -1274,6 +1281,13 @@ proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked
# this axis in both directions # this axis in both directions
result.pins.add((loc, square, direction)) result.pins.add((loc, square, direction))
result.pins.add((loc, square, -direction)) result.pins.add((loc, square, -direction))
if otherPiece.kind == Pawn and square.row == otherPiece.getStartRow():
# The pinned piece is a pawn which hasn't moved yet:
# we allow it to move two squares as well
if square.col == loc.col:
# The pawn can only push two squares if it's being pinned from the
# top
result.pins.add((loc, square, otherPiece.color.doublePush()))
else: else:
break break
else: else:
@ -1332,12 +1346,11 @@ proc updateAttackedSquares(self: ChessBoard) =
self.updateKingAttacks() self.updateKingAttacks()
proc removePiece(self: ChessBoard, location: Location, attack: bool = true, empty: bool = true) = proc removePiece(self: ChessBoard, location: Location, attack: bool = true) =
## Removes a piece from the board, updating necessary ## Removes a piece from the board, updating necessary
## metadata ## metadata
var piece = self.grid[location.row, location.col] var piece = self.grid[location.row, location.col]
if empty: self.grid[location.row, location.col] = emptyPiece()
self.grid[location.row, location.col] = emptyPiece()
case piece.color: case piece.color:
of White: of White:
case piece.kind: case piece.kind:
@ -1383,6 +1396,10 @@ proc movePiece(self: ChessBoard, move: Move, attack: bool = true) =
## not update attacked squares metadata, just ## not update attacked squares metadata, just
## positional info and the grid itself ## positional info and the grid itself
let piece = self.grid[move.startSquare.row, move.startSquare.col] let piece = self.grid[move.startSquare.row, move.startSquare.col]
let targetSquare = self.getPiece(move.targetSquare)
if targetSquare.color != None:
raise newException(AccessViolationDefect, &"attempted to overwrite a piece! {move}")
# Update positional metadata
case piece.color: case piece.color:
of White: of White:
case piece.kind: case piece.kind:
@ -1432,7 +1449,7 @@ proc movePiece(self: ChessBoard, move: Move, attack: bool = true) =
discard discard
# Empty out the starting square # Empty out the starting square
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 on the board
self.grid[move.targetSquare.row, move.targetSquare.col] = piece self.grid[move.targetSquare.row, move.targetSquare.col] = piece
if attack: if attack:
self.updateAttackedSquares() self.updateAttackedSquares()
@ -1459,7 +1476,7 @@ 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
enPassantTarget = self.getEnPassantTarget() enPassantTarget = emptyLocation()
# Needed to detect draw by the 50 move rule # Needed to detect draw by the 50 move rule
if piece.kind == Pawn or move.isCapture(): if piece.kind == Pawn or move.isCapture():
halfMoveClock = 0 halfMoveClock = 0
@ -1467,12 +1484,6 @@ proc doMove(self: ChessBoard, move: Move) =
inc(halfMoveClock) inc(halfMoveClock)
if piece.color == Black: if piece.color == Black:
inc(fullMoveCount) inc(fullMoveCount)
# En passant check
if enPassantTarget != emptyLocation():
let enPassantPawn = enPassantTarget + piece.color.topSide()
if self.grid[enPassantPawn.row, enPassantPawn.col].color == piece.color.opposite():
enPassantTarget = emptyLocation()
if move.isDoublePush(): if move.isDoublePush():
enPassantTarget = move.targetSquare + piece.color.bottomSide() enPassantTarget = move.targetSquare + piece.color.bottomSide()
@ -1566,11 +1577,16 @@ proc doMove(self: ChessBoard, move: Move) =
if move.isEnPassant(): if move.isEnPassant():
# Make the en passant pawn disappear # Make the en passant pawn disappear
self.removePiece(move.targetSquare + piece.color.bottomSide(), attack=false) self.removePiece(move.targetSquare + piece.color.bottomSide(), attack=false)
if move.isCapture():
# Get rid of captured pieces
self.removePiece(move.targetSquare, attack=false)
# Move the piece to its target square and update attack metadata
self.movePiece(move, attack=false)
if move.isPromotion(): if move.isPromotion():
# Move is a pawn promotion: get rid of the pawn # Move is a pawn promotion: get rid of the pawn
# and spawn a new piece # and spawn a new piece
self.removePiece(move.targetSquare, attack=false)
case move.getPromotionType(): case move.getPromotionType():
of PromoteToBishop: of PromoteToBishop:
self.spawnPiece(move.targetSquare, Piece(kind: Bishop, color: piece.color)) self.spawnPiece(move.targetSquare, Piece(kind: Bishop, color: piece.color))
@ -1582,16 +1598,10 @@ proc doMove(self: ChessBoard, move: Move) =
self.spawnPiece(move.targetSquare, Piece(kind: Queen, color: piece.color)) self.spawnPiece(move.targetSquare, Piece(kind: Queen, color: piece.color))
else: else:
discard discard
self.updateAttackedSquares() self.updateAttackedSquares()
if move.isCapture():
# Get rid of captured pieces
self.removePiece(move.targetSquare, attack=false, empty=false)
# Move the piece to its target square and update attack metadata
self.movePiece(move)
# TODO: Remove this, once I figure out what the heck is wrong # TODO: Remove this, once I figure out what the heck is wrong
# with updating the board representation # with updating the board representation
self.resetBoard() self.updateBoard()
proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) = proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) =
@ -1614,7 +1624,7 @@ proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) =
of Queen: of Queen:
self.position.pieces.white.queens.add(location) self.position.pieces.white.queens.add(location)
of King: of King:
self.position.pieces.white.king = location doAssert false, "attempted to spawn a white king"
else: else:
discard discard
of Black: of Black:
@ -1630,7 +1640,7 @@ proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) =
of Queen: of Queen:
self.position.pieces.black.queens.add(location) self.position.pieces.black.queens.add(location)
of King: of King:
self.position.pieces.black.king = location doAssert false, "attempted to spawn a black king"
else: else:
discard discard
else: else:
@ -1639,8 +1649,8 @@ proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) =
self.grid[location.row, location.col] = piece self.grid[location.row, location.col] = piece
proc resetBoard*(self: ChessBoard) = proc updateBoard*(self: ChessBoard) =
## Resets the internal grid representation ## Updates the internal grid representation
## according to the positional data stored ## according to the positional data stored
## in the chessboard ## in the chessboard
for i in 0..63: for i in 0..63:
@ -1672,7 +1682,7 @@ proc resetBoard*(self: ChessBoard) =
proc undoLastMove*(self: ChessBoard) = proc undoLastMove*(self: ChessBoard) =
if self.positions.len() > 0: if self.positions.len() > 0:
self.position = self.positions.pop() self.position = self.positions.pop()
self.resetBoard() self.updateBoard()
proc isLegal(self: ChessBoard, move: Move): bool {.inline.} = proc isLegal(self: ChessBoard, move: Move): bool {.inline.} =
@ -2048,7 +2058,7 @@ proc handlePositionCommand(board: var ChessBoard, command: seq[string]) =
return return
# Makes sure we don't leave the board in an invalid state if # Makes sure we don't leave the board in an invalid state if
# some error occurs # some error occurs
var tempBoard = newChessboard() var tempBoard: ChessBoard
case command[1]: case command[1]:
of "startpos": of "startpos":
tempBoard = newDefaultChessboard() tempBoard = newDefaultChessboard()
@ -2139,7 +2149,7 @@ const HELP_TEXT = """Nimfish help menu:
- move <move>: Perform the given move in algebraic notation - move <move>: Perform the given move in algebraic notation
- castle: Print castling rights for each side - castle: Print castling rights for each side
- check: Print if the current side to move is in check - check: Print if the current side to move is in check
- undo, u: Undoes the last move that was performed. Can be used in succession - undo, u: Undoes the last move. Can be used in succession
- turn: Print which side is to move - turn: Print which side is to move
- ep: Print the current en passant target - ep: Print the current en passant target
- pretty: Shorthand for "position pretty" - pretty: Shorthand for "position pretty"