Fixed position handling system

This commit is contained in:
Mattia Giambirtone 2023-10-17 16:38:43 +02:00
parent 4586b44ec1
commit a52783fa15
2 changed files with 66 additions and 62 deletions

View File

@ -100,7 +100,6 @@ type
## A chess board object ## A chess board object
grid: Matrix[Piece] grid: Matrix[Piece]
position: Position position: Position
positionIndex: int
# List of reached positions # List of reached positions
positions: seq[Position] positions: seq[Position]
@ -114,7 +113,7 @@ for _ in countup(0, 63):
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)
func opposite*(c: PieceColor): PieceColor {.inline.} = (if c == White: Black else: White) func opposite*(c: PieceColor): PieceColor {.inline.} = (if c == White: Black else: White)
proc algebraicToPosition*(s: string): Location {.inline.} proc algebraicToLocation*(s: string): Location {.inline.}
proc getCapture*(self: ChessBoard, move: Move): Location proc getCapture*(self: ChessBoard, move: Move): Location
proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move
proc makeMove*(self: ChessBoard, move: Move): Move proc makeMove*(self: ChessBoard, move: Move): Move
@ -208,6 +207,11 @@ proc getCastlingInformation*(self: ChessBoard): tuple[queen, king: bool] =
discard discard
proc getEnPassantTarget*(self: ChessBoard): Location =
## Returns the current en passant target square
return self.position.enPassantSquare.targetSquare
func getStartRow(piece: Piece): int {.inline.} = func getStartRow(piece: Piece): int {.inline.} =
## Retrieves the starting row of ## Retrieves the starting row of
## the given piece inside our 8x8 ## the given piece inside our 8x8
@ -251,7 +255,6 @@ proc newChessboard: ChessBoard =
move: emptyMove(), move: emptyMove(),
turn: White) turn: White)
result.positionIndex = 0
proc newChessboardFromFEN*(state: string): ChessBoard = proc newChessboardFromFEN*(state: string): ChessBoard =
@ -371,7 +374,7 @@ proc newChessboardFromFEN*(state: string): ChessBoard =
# Field is already uninitialized to the correct state # Field is already uninitialized to the correct state
discard discard
else: else:
result.position.enPassantSquare.targetSquare = state[index..index+1].algebraicToPosition() result.position.enPassantSquare.targetSquare = state[index..index+1].algebraicToLocation()
# Just for cleanliness purposes, we fill in the other metadata as # Just for cleanliness purposes, we fill in the other metadata as
# well # well
result.position.enPassantSquare.piece.color = result.getActiveColor() result.position.enPassantSquare.piece.color = result.getActiveColor()
@ -464,7 +467,7 @@ func rankToColumn(rank: int): int =
return indeces[rank - 1] return indeces[rank - 1]
proc algebraicToPosition*(s: string): Location {.inline.} = proc algebraicToLocation*(s: string): Location =
## Converts a square location from algebraic ## Converts a square location from algebraic
## notation to its corresponding row and column ## notation to its corresponding row and column
## in the chess grid (0 indexed) ## in the chess grid (0 indexed)
@ -483,10 +486,16 @@ proc algebraicToPosition*(s: string): Location {.inline.} =
return (rank, file) return (rank, file)
proc locationToAlgebraic*(loc: Location): string =
## Converts a location from our internal row, column
## notation to a square in algebraic notation
return &"{char(uint8(loc.col) + uint8('a'))}{char(uint8(loc.row) + uint8('0'))}"
proc getPiece*(self: ChessBoard, square: string): Piece = proc getPiece*(self: ChessBoard, square: string): Piece =
## Gets the piece on the given square ## Gets the piece on the given square
## in algebraic notation ## in algebraic notation
let loc = square.algebraicToPosition() let loc = square.algebraicToLocation()
return self.grid[loc.row, loc.col] return self.grid[loc.row, loc.col]
@ -570,7 +579,7 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
continue continue
# Move is just a pawn push # Move is just a pawn push
result.add(Move(startSquare: location, targetSquare: newLocation, piece: piece)) 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] =
## Generates moves for the sliding piece in the given location ## Generates moves for the sliding piece in the given location
@ -695,7 +704,7 @@ proc generateMoves(self: ChessBoard, location: Location): seq[Move] =
proc getAttackers*(self: ChessBoard, square: string): seq[Piece] = proc getAttackers*(self: ChessBoard, square: string): seq[Piece] =
## Returns all the attackers of the given square ## Returns all the attackers of the given square
let loc = square.algebraicToPosition() let loc = square.algebraicToLocation()
for move in self.position.attacked.black: for move in self.position.attacked.black:
if move.targetSquare == loc: if move.targetSquare == loc:
result.add(move.piece) result.add(move.piece)
@ -707,7 +716,7 @@ proc getAttackers*(self: ChessBoard, square: string): seq[Piece] =
proc getAttackersFor*(self: ChessBoard, square: string, color: PieceColor): seq[Piece] = proc getAttackersFor*(self: ChessBoard, square: string, color: PieceColor): seq[Piece] =
## Returns all the attackers of the given square ## Returns all the attackers of the given square
## for the given color ## for the given color
let loc = square.algebraicToPosition() let loc = square.algebraicToLocation()
case color: case color:
of White: of White:
for move in self.position.attacked.black: for move in self.position.attacked.black:
@ -755,7 +764,7 @@ proc isAttacked*(self: ChessBoard, loc: Location): bool =
proc isAttacked*(self: ChessBoard, square: string): bool = proc isAttacked*(self: ChessBoard, square: string): bool =
## Returns whether the given square is attacked ## Returns whether the given square is attacked
## by its opponent ## by its opponent
return self.isAttacked(square.algebraicToPosition()) return self.isAttacked(square.algebraicToLocation())
proc updateAttackedSquares(self: ChessBoard) = proc updateAttackedSquares(self: ChessBoard) =
@ -850,14 +859,6 @@ proc removePiece(self: ChessBoard, location: Location) =
discard 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) = proc movePiece(self: ChessBoard, move: Move) =
## Internal helper to move a piece. Does ## Internal helper to move a piece. Does
## not update attacked squares, just position ## not update attacked squares, just position
@ -925,11 +926,7 @@ proc updatePositions(self: ChessBoard, move: Move) =
## 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():
# Move has captured a piece: remove it as well. We call a helper instead self.position.captured = self.grid[capture.row, capture.col]
# 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.handleCapture(move)
# Update the positional metadata of the moving piece # Update the positional metadata of the moving piece
self.movePiece(move) self.movePiece(move)
@ -970,10 +967,6 @@ proc doMove(self: ChessBoard, move: Move) =
self.position.halfMoveClock = 0 self.position.halfMoveClock = 0
if (self.position.halfMoveClock and 1) == 0: # Equivalent to (x mod 2) == 0, just much faster if (self.position.halfMoveClock and 1) == 0: # Equivalent to (x mod 2) == 0, just much faster
inc(self.position.fullMoveCount) inc(self.position.fullMoveCount)
# En passant is possible only immediately after the
# pawn has moved
if self.position.enPassantSquare != emptyMove() and self.position.enPassantSquare.piece.color == self.getActiveColor().opposite():
self.position.enPassantSquare = emptyMove()
# TODO: Castling # TODO: Castling
self.position.move = move self.position.move = move
@ -987,16 +980,24 @@ proc doMove(self: ChessBoard, move: Move) =
# Create new position with # Create new position with
var newPos = Position(plyFromRoot: self.position.plyFromRoot + 1, var newPos = Position(plyFromRoot: self.position.plyFromRoot + 1,
captured: emptyPiece(), captured: emptyPiece(),
turn: self.position.turn.opposite(), turn: self.getActiveColor().opposite,
# Inherit values from current position # Inherit values from current position
# (they are already up to date by this point) # (they are already up to date by this point)
castling: self.position.castling, castling: self.position.castling,
enPassantSquare: self.position.enPassantSquare, attacked: self.position.attacked,
attacked: (@[], @[]) # Updated at the next call to doMove()
move: emptyMove(),
pieces: self.position.pieces,
) )
# 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())
else:
newPos.enPassantSquare = emptyMove()
self.position = newPos self.position = newPos
inc(self.positionIndex)
proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) = proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) =
@ -1042,43 +1043,41 @@ proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) =
proc undoLastMove*(self: ChessBoard): Move {.discardable.} = proc undoLastMove*(self: ChessBoard): Move {.discardable.} =
## Undoes the last move, restoring any captured pieces, ## Undoes the last move, restoring the previous position.
## as well castling and en passant status. If there are ## If there are no positions to roll back to to, this is a
## no moves to undo, this is a no-op. Returns the move ## no-op. Returns the move that was performed (may be empty)
## that was performed (which may be an empty move)
result = emptyMove() result = emptyMove()
if self.positions.len() == 0: if self.positions.len() == 0:
return return
let positionIndex = max(0, self.positionIndex - 1) var
if positionIndex in 0..self.positions.high(): previous = self.positions[^1]
self.positionIndex = positionIndex oppositeMove = Move(piece: previous.move.piece, targetSquare: previous.move.startSquare, startSquare: previous.move.targetSquare)
self.position = self.positions[positionIndex] self.removePiece(previous.move.startSquare)
let self.position = previous
currentMove = self.position.move if previous.move != emptyMove():
oppositeMove = Move(piece: currentMove.piece, targetSquare: currentMove.startSquare, startSquare: currentMove.targetSquare) self.spawnPiece(previous.move.startSquare, previous.move.piece)
self.spawnPiece(currentMove.startSquare, currentMove.piece)
if self.position.captured != emptyPiece():
self.spawnPiece(self.position.move.targetSquare, self.position.captured)
self.updateAttackedSquares() self.updateAttackedSquares()
self.updatePositions(oppositeMove) self.updatePositions(oppositeMove)
return self.position.move 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 =
## Returns whether the given move is legal
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) # 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 # or it's not this player's turn to move
if move.piece.kind == Empty or move.piece.color != self.getActiveColor(): if (move.piece.kind == Empty and move.targetSquare != self.getEnPassantTarget()) or move.piece.color != self.getActiveColor():
return false return false
var destination = self.grid[move.targetSquare.row, move.targetSquare.col] var destination = self.grid[move.targetSquare.row, move.targetSquare.col]
# Destination square is occupied by a piece of the same color as the piece # Destination square is occupied by a friendly piece
# being moved: illegal!
if destination.kind != Empty and destination.color == self.getActiveColor(): if destination.kind != Empty and destination.color == self.getActiveColor():
return false return false
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,
# pinned, or otherwise invalid move) # pinned or otherwise invalid move)
return false return false
self.doMove(move) self.doMove(move)
defer: self.undoLastMove() defer: self.undoLastMove()
@ -1093,12 +1092,11 @@ proc checkMove(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.checkMove(move): if not self.isLegal(move):
return emptyMove() return emptyMove()
self.doMove(result) self.doMove(result)
proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.discardable.} = proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.discardable.} =
## Makes a move on the board from the chosen start square to ## Makes a move on the board from the chosen start square to
## the chosen target square, ensuring it is legal (turns are ## the chosen target square, ensuring it is legal (turns are
@ -1108,8 +1106,8 @@ proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.disc
## illegal, but the move's piece kind will be Empty (its color will be None ## illegal, but the move's piece kind will be Empty (its color will be None
## too) and the locations will both be set to the tuple (-1, -1) ## too) and the locations will both be set to the tuple (-1, -1)
var var
startLocation = startSquare.algebraicToPosition() startLocation = startSquare.algebraicToLocation()
targetLocation = targetSquare.algebraicToPosition() targetLocation = targetSquare.algebraicToLocation()
result = Move(startSquare: startLocation, targetSquare: targetLocation, piece: self.grid[startLocation.row, startLocation.col]) result = Move(startSquare: startLocation, targetSquare: targetLocation, piece: self.grid[startLocation.row, startLocation.col])
return self.makeMove(result) return self.makeMove(result)
@ -1120,8 +1118,6 @@ proc makeMove*(self: ChessBoard, startSquare, targetSquare: Location): Move {.di
return self.makeMove(result) return self.makeMove(result)
proc `$`*(self: ChessBoard): string = proc `$`*(self: ChessBoard): string =
result &= "- - - - - - - -" result &= "- - - - - - - -"
for i, row in self.grid: for i, row in self.grid:

View File

@ -9,7 +9,7 @@ import std/strutils
when isMainModule: when isMainModule:
setControlCHook(proc () {.noconv.} = echo ""; quit(0)) setControlCHook(proc () {.noconv.} = echo ""; quit(0))
var var
board = newChessboardFromFEN("rnbqkbnr/8/8/8/8/8/8/RNBQKBNR w KQkq - 0 1") board = newChessboardFromFEN("rnbqkbnr/2p/8/8/8/8/P7/RNBQKBNR w KQkq - 0 1")
data: string data: string
move: Move move: Move
@ -17,9 +17,17 @@ when isMainModule:
while true: while true:
echo &"{board.pretty()}" echo &"{board.pretty()}"
echo &"Turn: {board.getActiveColor()}" echo &"Turn: {board.getActiveColor()}"
stdout.write(&"En passant target: ")
if board.getEnPassantTarget() != emptyLocation():
echo board.getEnPassantTarget()
else:
echo "None"
stdout.write(&"Check: ")
if board.inCheck(): if board.inCheck():
echo &"Check!" echo &"Yes"
stdout.write("Move (from, to) -> ") else:
echo "No"
stdout.write("\nMove -> ")
try: try:
data = readLine(stdin).strip(chars={'\0', ' '}) data = readLine(stdin).strip(chars={'\0', ' '})
except IOError: except IOError: