Fixed position handling system
This commit is contained in:
parent
79477fe077
commit
b4ef8b4a2e
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue