From 54a6217bd300b3e68d63337b99cb26484d3ce1fe Mon Sep 17 00:00:00 2001 From: nocturn9x Date: Wed, 10 Apr 2024 13:45:29 +0200 Subject: [PATCH] More movegen bug fixes (close!) --- src/Chess/board.nim | 112 ++++++++++++++++++++++++-------------------- 1 file changed, 61 insertions(+), 51 deletions(-) diff --git a/src/Chess/board.nim b/src/Chess/board.nim index 05537e6..7098dad 100644 --- a/src/Chess/board.nim +++ b/src/Chess/board.nim @@ -76,7 +76,7 @@ type Position* = ref object ## 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]] # Number of half-moves that were performed # to reach this position starting from the @@ -95,7 +95,7 @@ type pieces: tuple[white: Pieces, black: Pieces] # Squares attacked by both sides 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] # Active color turn: PieceColor @@ -105,14 +105,14 @@ type # The actual board where pieces live # (flattened 8x8 matrix) - grid: seq[Piece] + grid: array[64, Piece] # The current position position: Position # List of all previously reached positions 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 emptyLocation*: Location {.inline.} = (-1 , -1) @@ -145,7 +145,7 @@ proc extend[T](self: var seq[T], other: openarray[T]) {.inline.} = for x in other: 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 # 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 = ## Returns a new, empty chessboard new(result) - # Turns our flat sequence into an 8x8 grid - result.grid = newSeqOfCap[Piece](64) - for _ in 0..63: - result.grid.add(emptyPiece()) + for i in 0..63: + result.grid[i] = emptyPiece() result.position = Position(attacked: (@[], @[]), enPassantSquare: emptyLocation(), turn: White, @@ -313,10 +311,10 @@ proc newChessboard: ChessBoard = func coordToIndex(row, col: int): int {.inline.} = (row * 8) + col -func `[]`(self: seq[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 -func `[]`(self: seq[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 +func `[]`(self: array[64, Piece], row, column: Natural): Piece {.inline.} = self[coordToIndex(row, column)] +proc `[]=`(self: var array[64, Piece], row, column: Natural, piece: Piece) {.inline.} = self[coordToIndex(row, column)] = piece +func `[]`(self: array[64, Piece], loc: Location): Piece {.inline.} = self[loc.row, loc.col] +proc `[]=`(self: var array[64, Piece], loc: Location, piece: Piece) {.inline.} = self[loc.row, loc.col] = piece proc newChessboardFromFEN*(fen: string): ChessBoard = @@ -535,10 +533,10 @@ func rankToColumn(rank: int): int8 {.inline.} = return indeces[rank - 1] -func rowToFile(row: int): int {.inline.} = +func rowToFile(row: int): int8 {.inline.} = ## Converts a row into our grid into ## 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] @@ -641,7 +639,9 @@ func getFlags*(move: Move): seq[MoveFlag] = 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 if color == None: color = self.getActiveColor() @@ -803,7 +803,8 @@ proc getCheckResolutions(self: ChessBoard, color: PieceColor): seq[Location] = let attacker = attackers[0] attackerPiece = self.grid[attacker.row, attacker.col] - attack = self.getAttackFor(attacker, king) + + var attack = self.getAttackFor(attacker, king) # Capturing the piece resolves the check result.add(attacker) # 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: break 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 - 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: # Target square contains an enemy piece: capture # it and stop going any further @@ -950,7 +957,7 @@ proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = # Can't capture the king result.add(Move(startSquare: location, targetSquare: square, flags: Capture.uint16)) break - # Target square is empty + # Target square is empty, keep going 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 # they can capture on. Sneaky fucks) 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 for loc in self.position.pieces.black.pawns: 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) = @@ -1258,7 +1265,7 @@ proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked # We found an enemy piece that is not # the enemy king. We don't break out # immediately because we first want - # to check if we've pinned a piece + # to check if we've pinned it to the king var otherSquare: Location = square behindPiece: Piece @@ -1274,6 +1281,13 @@ proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked # this axis in both directions 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: break else: @@ -1332,12 +1346,11 @@ proc updateAttackedSquares(self: ChessBoard) = 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 ## metadata 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: of White: case piece.kind: @@ -1383,6 +1396,10 @@ proc movePiece(self: ChessBoard, move: Move, attack: bool = true) = ## not update attacked squares metadata, just ## positional info and the grid itself 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: of White: case piece.kind: @@ -1432,7 +1449,7 @@ proc movePiece(self: ChessBoard, move: Move, attack: bool = true) = discard # Empty out the starting square 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 if attack: self.updateAttackedSquares() @@ -1459,7 +1476,7 @@ proc doMove(self: ChessBoard, move: Move) = halfMoveClock = self.position.halfMoveClock fullMoveCount = self.position.fullMoveCount castlingAvailable = self.position.castlingAvailable - enPassantTarget = self.getEnPassantTarget() + enPassantTarget = emptyLocation() # Needed to detect draw by the 50 move rule if piece.kind == Pawn or move.isCapture(): halfMoveClock = 0 @@ -1467,12 +1484,6 @@ proc doMove(self: ChessBoard, move: Move) = inc(halfMoveClock) if piece.color == Black: 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(): enPassantTarget = move.targetSquare + piece.color.bottomSide() @@ -1566,11 +1577,16 @@ proc doMove(self: ChessBoard, move: Move) = if move.isEnPassant(): # Make the en passant pawn disappear 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(): # Move is a pawn promotion: get rid of the pawn # and spawn a new piece - self.removePiece(move.targetSquare, attack=false) case move.getPromotionType(): of PromoteToBishop: 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)) else: discard - 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) + self.updateAttackedSquares() # TODO: Remove this, once I figure out what the heck is wrong # with updating the board representation - self.resetBoard() + self.updateBoard() proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) = @@ -1614,7 +1624,7 @@ proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) = of Queen: self.position.pieces.white.queens.add(location) of King: - self.position.pieces.white.king = location + doAssert false, "attempted to spawn a white king" else: discard of Black: @@ -1630,7 +1640,7 @@ proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) = of Queen: self.position.pieces.black.queens.add(location) of King: - self.position.pieces.black.king = location + doAssert false, "attempted to spawn a black king" else: discard else: @@ -1639,8 +1649,8 @@ proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) = self.grid[location.row, location.col] = piece -proc resetBoard*(self: ChessBoard) = - ## Resets the internal grid representation +proc updateBoard*(self: ChessBoard) = + ## Updates the internal grid representation ## according to the positional data stored ## in the chessboard for i in 0..63: @@ -1672,7 +1682,7 @@ proc resetBoard*(self: ChessBoard) = proc undoLastMove*(self: ChessBoard) = if self.positions.len() > 0: self.position = self.positions.pop() - self.resetBoard() + self.updateBoard() proc isLegal(self: ChessBoard, move: Move): bool {.inline.} = @@ -2048,7 +2058,7 @@ proc handlePositionCommand(board: var ChessBoard, command: seq[string]) = return # Makes sure we don't leave the board in an invalid state if # some error occurs - var tempBoard = newChessboard() + var tempBoard: ChessBoard case command[1]: of "startpos": tempBoard = newDefaultChessboard() @@ -2139,7 +2149,7 @@ const HELP_TEXT = """Nimfish help menu: - move : Perform the given move in algebraic notation - castle: Print castling rights for each side - 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 - ep: Print the current en passant target - pretty: Shorthand for "position pretty"