Fixed position handling system

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

View File

@ -100,7 +100,6 @@ type
## A chess board object
grid: Matrix[Piece]
position: Position
positionIndex: int
# List of reached positions
positions: seq[Position]
@ -114,7 +113,7 @@ for _ in countup(0, 63):
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 algebraicToPosition*(s: string): Location {.inline.}
proc algebraicToLocation*(s: string): Location {.inline.}
proc getCapture*(self: ChessBoard, move: Move): Location
proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move
proc makeMove*(self: ChessBoard, move: Move): Move
@ -208,6 +207,11 @@ proc getCastlingInformation*(self: ChessBoard): tuple[queen, king: bool] =
discard
proc getEnPassantTarget*(self: ChessBoard): Location =
## Returns the current en passant target square
return self.position.enPassantSquare.targetSquare
func getStartRow(piece: Piece): int {.inline.} =
## Retrieves the starting row of
## the given piece inside our 8x8
@ -251,7 +255,6 @@ proc newChessboard: ChessBoard =
move: emptyMove(),
turn: White)
result.positionIndex = 0
proc newChessboardFromFEN*(state: string): ChessBoard =
@ -371,7 +374,7 @@ proc newChessboardFromFEN*(state: string): ChessBoard =
# Field is already uninitialized to the correct state
discard
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
# well
result.position.enPassantSquare.piece.color = result.getActiveColor()
@ -464,7 +467,7 @@ func rankToColumn(rank: int): int =
return indeces[rank - 1]
proc algebraicToPosition*(s: string): Location {.inline.} =
proc algebraicToLocation*(s: string): Location =
## Converts a square location from algebraic
## notation to its corresponding row and column
## in the chess grid (0 indexed)
@ -483,10 +486,16 @@ proc algebraicToPosition*(s: string): Location {.inline.} =
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 =
## Gets the piece on the given square
## in algebraic notation
let loc = square.algebraicToPosition()
let loc = square.algebraicToLocation()
return self.grid[loc.row, loc.col]
@ -570,7 +579,7 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
continue
# Move is just a pawn push
result.add(Move(startSquare: location, targetSquare: newLocation, piece: piece))
proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
## 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] =
## Returns all the attackers of the given square
let loc = square.algebraicToPosition()
let loc = square.algebraicToLocation()
for move in self.position.attacked.black:
if move.targetSquare == loc:
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] =
## Returns all the attackers of the given square
## for the given color
let loc = square.algebraicToPosition()
let loc = square.algebraicToLocation()
case color:
of White:
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 =
## Returns whether the given square is attacked
## by its opponent
return self.isAttacked(square.algebraicToPosition())
return self.isAttacked(square.algebraicToLocation())
proc updateAttackedSquares(self: ChessBoard) =
@ -850,14 +859,6 @@ 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
@ -925,11 +926,7 @@ proc updatePositions(self: ChessBoard, move: Move) =
## the pieces on the board after a move
let capture = self.getCapture(move)
if capture != emptyLocation():
# Move has captured a piece: remove it as well. We call a helper instead
# 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)
self.position.captured = self.grid[capture.row, capture.col]
# Update the positional metadata of the moving piece
self.movePiece(move)
@ -970,10 +967,6 @@ proc doMove(self: ChessBoard, move: Move) =
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.position.enPassantSquare != emptyMove() and self.position.enPassantSquare.piece.color == self.getActiveColor().opposite():
self.position.enPassantSquare = emptyMove()
# TODO: Castling
self.position.move = move
@ -987,16 +980,24 @@ proc doMove(self: ChessBoard, move: Move) =
# Create new position with
var newPos = Position(plyFromRoot: self.position.plyFromRoot + 1,
captured: emptyPiece(),
turn: self.position.turn.opposite(),
turn: self.getActiveColor().opposite,
# Inherit values from current position
# (they are already up to date by this point)
castling: self.position.castling,
enPassantSquare: self.position.enPassantSquare,
attacked: (@[], @[])
attacked: self.position.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
inc(self.positionIndex)
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.} =
## 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)
## Undoes the last move, restoring the previous position.
## If there are no positions to roll back to to, this is a
## no-op. Returns the move that was performed (may be empty)
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)
var
previous = self.positions[^1]
oppositeMove = Move(piece: previous.move.piece, targetSquare: previous.move.startSquare, startSquare: previous.move.targetSquare)
self.removePiece(previous.move.startSquare)
self.position = previous
if previous.move != emptyMove():
self.spawnPiece(previous.move.startSquare, previous.move.piece)
self.updateAttackedSquares()
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)
# or it is of the wrong color for which turn it is to move
if move.piece.kind == Empty or move.piece.color != self.getActiveColor():
# 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():
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!
# Destination square is occupied by a friendly piece
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)
# pinned or otherwise invalid move)
return false
self.doMove(move)
defer: self.undoLastMove()
@ -1093,12 +1092,11 @@ proc checkMove(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.checkMove(move):
if not self.isLegal(move):
return emptyMove()
self.doMove(result)
proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.discardable.} =
## Makes a move on the board from the chosen start square to
## 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
## too) and the locations will both be set to the tuple (-1, -1)
var
startLocation = startSquare.algebraicToPosition()
targetLocation = targetSquare.algebraicToPosition()
startLocation = startSquare.algebraicToLocation()
targetLocation = targetSquare.algebraicToLocation()
result = Move(startSquare: startLocation, targetSquare: targetLocation, piece: self.grid[startLocation.row, startLocation.col])
return self.makeMove(result)
@ -1120,8 +1118,6 @@ proc makeMove*(self: ChessBoard, startSquare, targetSquare: Location): Move {.di
return self.makeMove(result)
proc `$`*(self: ChessBoard): string =
result &= "- - - - - - - -"
for i, row in self.grid:

View File

@ -9,7 +9,7 @@ import std/strutils
when isMainModule:
setControlCHook(proc () {.noconv.} = echo ""; quit(0))
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
move: Move
@ -17,9 +17,17 @@ when isMainModule:
while true:
echo &"{board.pretty()}"
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():
echo &"Check!"
stdout.write("Move (from, to) -> ")
echo &"Yes"
else:
echo "No"
stdout.write("\nMove -> ")
try:
data = readLine(stdin).strip(chars={'\0', ' '})
except IOError: