2023-03-18 18:14:30 +01:00
|
|
|
# Copyright 2023 Mattia Giambirtone & All Contributors
|
|
|
|
#
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
#
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
#
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
# limitations under the License.
|
|
|
|
import ../util/matrix
|
|
|
|
|
2023-10-12 11:55:12 +02:00
|
|
|
export matrix
|
2023-03-18 18:14:30 +01:00
|
|
|
|
|
|
|
import std/strutils
|
2023-10-12 10:14:37 +02:00
|
|
|
import std/strformat
|
2023-10-20 02:23:07 +02:00
|
|
|
import std/sequtils
|
2023-03-18 18:14:30 +01:00
|
|
|
|
|
|
|
|
|
|
|
type
|
2023-10-15 16:53:44 +02:00
|
|
|
# Useful type aliases
|
2023-10-18 10:45:54 +02:00
|
|
|
Location* = tuple[row, col: int8]
|
2023-10-21 18:19:41 +02:00
|
|
|
|
|
|
|
Attacked = seq[tuple[source, dest: Location]]
|
2023-10-15 16:53:44 +02:00
|
|
|
|
2023-10-16 09:39:17 +02:00
|
|
|
Pieces = tuple[king: Location, queens: seq[Location], rooks: seq[Location],
|
2023-10-13 12:26:14 +02:00
|
|
|
bishops: seq[Location], knights: seq[Location],
|
|
|
|
pawns: seq[Location]]
|
2023-10-15 16:53:44 +02:00
|
|
|
|
2023-03-18 18:14:30 +01:00
|
|
|
PieceColor* = enum
|
2023-10-15 16:53:44 +02:00
|
|
|
## A piece color enumeration
|
2023-10-18 10:45:54 +02:00
|
|
|
None = 0'i8,
|
2023-03-18 18:14:30 +01:00
|
|
|
White,
|
|
|
|
Black
|
2023-10-15 16:53:44 +02:00
|
|
|
|
2023-03-18 18:14:30 +01:00
|
|
|
PieceKind* = enum
|
2023-10-15 16:53:44 +02:00
|
|
|
## A chess piece enumeration
|
2023-10-13 12:26:14 +02:00
|
|
|
Empty = '\0', # No piece
|
2023-03-18 18:14:30 +01:00
|
|
|
Bishop = 'b',
|
|
|
|
King = 'k'
|
|
|
|
Knight = 'n',
|
|
|
|
Pawn = 'p',
|
|
|
|
Queen = 'q',
|
|
|
|
Rook = 'r',
|
2023-10-15 16:53:44 +02:00
|
|
|
|
2023-03-18 18:14:30 +01:00
|
|
|
Piece* = object
|
2023-10-15 16:53:44 +02:00
|
|
|
## A chess piece
|
2023-03-18 18:14:30 +01:00
|
|
|
color*: PieceColor
|
|
|
|
kind*: PieceKind
|
2023-10-15 16:53:44 +02:00
|
|
|
|
2023-10-16 14:55:43 +02:00
|
|
|
MoveFlag* = enum
|
|
|
|
## An enumeration of move flags
|
2023-10-20 02:23:07 +02:00
|
|
|
Default = 0'i8, # No flag
|
2023-10-21 18:19:41 +02:00
|
|
|
EnPassant, # Move is a capture with en passant
|
2023-10-20 02:23:07 +02:00
|
|
|
Capture, # Move is a capture
|
|
|
|
DoublePush, # Move is a double pawn push
|
|
|
|
# Castling metadata
|
2023-10-17 22:16:01 +02:00
|
|
|
CastleLong,
|
|
|
|
CastleShort,
|
2023-10-16 14:55:43 +02:00
|
|
|
XRay, # Move is an X-ray attack
|
2023-10-20 02:23:07 +02:00
|
|
|
# Pawn promotion metadata
|
2023-10-16 14:55:43 +02:00
|
|
|
PromoteToQueen,
|
|
|
|
PromoteToRook,
|
|
|
|
PromoteToBishop,
|
|
|
|
PromoteToKnight
|
|
|
|
|
|
|
|
Move* = object
|
2023-10-15 16:53:44 +02:00
|
|
|
## A chess move
|
2023-03-18 18:14:30 +01:00
|
|
|
piece*: Piece
|
2023-10-15 16:53:44 +02:00
|
|
|
startSquare*: Location
|
|
|
|
targetSquare*: Location
|
2023-10-16 14:55:43 +02:00
|
|
|
flag*: MoveFlag
|
2023-10-15 16:53:44 +02:00
|
|
|
|
2023-10-17 15:08:46 +02:00
|
|
|
Position* = ref object
|
|
|
|
## A chess position
|
|
|
|
move: Move
|
2023-10-17 22:16:01 +02:00
|
|
|
# Did the rooks on either side/the king move?
|
|
|
|
castlingAvailable: tuple[white, black: tuple[queen, king: bool]]
|
2023-10-17 15:08:46 +02:00
|
|
|
# Number of half-moves that were performed
|
|
|
|
# to reach this position starting from the
|
|
|
|
# root of the tree
|
2023-10-18 10:45:54 +02:00
|
|
|
plyFromRoot: int16
|
2023-03-18 18:14:30 +01:00
|
|
|
# Number of half moves since
|
2023-10-12 10:14:37 +02:00
|
|
|
# last piece capture or pawn movement.
|
|
|
|
# Used for the 50-move rule
|
2023-10-18 10:45:54 +02:00
|
|
|
halfMoveClock: int8
|
2023-03-18 18:14:30 +01:00
|
|
|
# Full move counter. Increments
|
|
|
|
# every 2 ply
|
2023-10-18 10:45:54 +02:00
|
|
|
fullMoveCount: int16
|
2023-03-18 18:14:30 +01:00
|
|
|
# En passant target square (see https://en.wikipedia.org/wiki/En_passant)
|
|
|
|
# If en passant is not possible, both the row and
|
|
|
|
# column of the position will be set to -1
|
2023-10-16 14:55:43 +02:00
|
|
|
enPassantSquare*: Move
|
2023-10-12 11:55:12 +02:00
|
|
|
# Locations of all pieces
|
|
|
|
pieces: tuple[white: Pieces, black: Pieces]
|
2023-10-16 23:02:58 +02:00
|
|
|
# Potential attacking moves for black and white
|
2023-10-21 18:19:41 +02:00
|
|
|
attacked: tuple[white: Attacked, black: Attacked]
|
2023-10-17 15:08:46 +02:00
|
|
|
# Has any piece been captured to reach this position?
|
|
|
|
captured: Piece
|
|
|
|
# Active color
|
|
|
|
turn: PieceColor
|
|
|
|
|
|
|
|
|
|
|
|
ChessBoard* = ref object
|
|
|
|
## A chess board object
|
|
|
|
grid: Matrix[Piece]
|
|
|
|
position: Position
|
|
|
|
# List of reached positions
|
|
|
|
positions: seq[Position]
|
2023-10-12 11:55:12 +02:00
|
|
|
|
2023-03-18 18:14:30 +01:00
|
|
|
|
|
|
|
# Initialized only once, copied every time
|
|
|
|
var empty: seq[Piece] = @[]
|
|
|
|
for _ in countup(0, 63):
|
|
|
|
empty.add(Piece(kind: Empty, color: None))
|
|
|
|
|
|
|
|
|
2023-10-15 16:53:44 +02:00
|
|
|
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)
|
2023-10-17 16:38:43 +02:00
|
|
|
proc algebraicToLocation*(s: string): Location {.inline.}
|
2023-10-17 22:16:01 +02:00
|
|
|
func getCapture*(self: ChessBoard, move: Move): Location
|
2023-10-21 18:19:41 +02:00
|
|
|
proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.discardable.}
|
|
|
|
proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.}
|
|
|
|
proc makeMove*(self: ChessBoard, startSquare, targetSquare: Location): Move {.discardable.}
|
2023-10-16 14:55:43 +02:00
|
|
|
func emptyMove*: Move {.inline.} = Move(startSquare: emptyLocation(), targetSquare: emptyLocation(), piece: emptyPiece())
|
2023-10-16 22:14:58 +02:00
|
|
|
func `+`*(a, b: Location): Location = (a.row + b.row, a.col + b.col)
|
2023-10-18 10:45:54 +02:00
|
|
|
func isValid*(a: Location): bool {.inline.} = a.row in 0..7 and a.col in 0..7
|
2023-10-16 22:14:58 +02:00
|
|
|
proc generateMoves(self: ChessBoard, location: Location): seq[Move]
|
2023-10-17 22:16:01 +02:00
|
|
|
proc isAttacked*(self: ChessBoard, loc: Location): bool
|
2023-10-21 18:19:41 +02:00
|
|
|
proc undoMove*(self: ChessBoard, move: Move)
|
2023-10-20 02:23:07 +02:00
|
|
|
proc isLegal(self: ChessBoard, move: Move, keep: bool = false): bool
|
2023-10-18 10:45:54 +02:00
|
|
|
proc doMove(self: ChessBoard, move: Move)
|
2023-10-20 02:23:07 +02:00
|
|
|
proc isLegalFast(self: ChessBoard, move: Move, keep: bool = false): bool
|
|
|
|
proc pretty*(self: ChessBoard): string
|
2023-10-21 18:19:41 +02:00
|
|
|
proc spawnPiece(self: ChessBoard, location: Location, piece: Piece)
|
|
|
|
proc updateAttackedSquares(self: ChessBoard)
|
2023-10-16 22:14:58 +02:00
|
|
|
|
2023-10-17 12:42:15 +02:00
|
|
|
# Due to our board layout, directions of movement are reversed for white/black so
|
|
|
|
# we need these helpers to avoid going mad with integer tuples and minus signs
|
|
|
|
# everywhere
|
2023-10-17 22:16:01 +02:00
|
|
|
func topLeftDiagonal(color: PieceColor): Location {.inline.} = (if color == White: (-1, -1) else: (1, 1))
|
|
|
|
func topRightDiagonal(color: PieceColor): Location {.inline.} = (if color == White: (-1, 1) else: (1, -1))
|
|
|
|
func bottomLeftDiagonal(color: PieceColor): Location {.inline.} = (if color == White: (1, -1) else: (-1, 1))
|
|
|
|
func bottomRightDiagonal(color: PieceColor): Location {.inline.} = (if color == White: (1, 1) else: (-1, -1))
|
|
|
|
func leftSide(color: PieceColor): Location {.inline.} = (if color == White: (0, -1) else: (0, 1))
|
|
|
|
func rightSide(color: PieceColor): Location {.inline.} = (if color == White: (0, 1) else: (0, -1))
|
2023-10-21 18:19:41 +02:00
|
|
|
func topSide(color: PieceColor): Location {.inline.} = (-1, 0)
|
2023-10-17 22:16:01 +02:00
|
|
|
func bottomSide(color: PieceColor): Location {.inline.} = (if color == White: (1, 0) else: (-1, 0))
|
|
|
|
func forward(color: PieceColor): Location {.inline.} = (if color == White: (-1, 0) else: (1, 0))
|
|
|
|
func doublePush(color: PieceColor): Location {.inline.} = (if color == White: (-2, 0) else: (2, 0))
|
2023-10-20 02:23:07 +02:00
|
|
|
func longCastleKing: Location {.inline.} = (0, -2)
|
|
|
|
func shortCastleKing: Location {.inline.} = (0, 2)
|
|
|
|
func longCastleRook: Location {.inline.} = (0, 3)
|
|
|
|
func shortCastleRook: Location {.inline.} = (0, -2)
|
2023-10-17 22:16:01 +02:00
|
|
|
func kingSideRook(color: PieceColor): Location {.inline.} = (if color == White: (7, 7) else: (0, 7))
|
|
|
|
func queenSideRook(color: PieceColor): Location {.inline.} = (if color == White: (7, 0) else: (0, 0))
|
|
|
|
|
|
|
|
|
|
|
|
func bottomLeftKnightMove(color: PieceColor, long: bool = true): Location {.inline.} =
|
|
|
|
if color == White:
|
2023-10-17 12:42:15 +02:00
|
|
|
if long:
|
2023-10-18 10:45:54 +02:00
|
|
|
return (2, -1)
|
2023-10-17 12:42:15 +02:00
|
|
|
else:
|
2023-10-20 02:23:07 +02:00
|
|
|
return (1, -2)
|
2023-10-17 22:16:01 +02:00
|
|
|
elif color == Black:
|
2023-10-17 12:42:15 +02:00
|
|
|
if long:
|
2023-10-21 18:19:41 +02:00
|
|
|
return (-2, 1)
|
2023-10-17 12:42:15 +02:00
|
|
|
else:
|
2023-10-21 18:19:41 +02:00
|
|
|
return (1, -2)
|
2023-10-17 12:42:15 +02:00
|
|
|
|
|
|
|
|
2023-10-17 22:16:01 +02:00
|
|
|
func bottomRightKnightMove(color: PieceColor, long: bool = true): Location {.inline.} =
|
|
|
|
if color == White:
|
2023-10-17 12:42:15 +02:00
|
|
|
if long:
|
2023-10-20 02:23:07 +02:00
|
|
|
return (2, 1)
|
2023-10-17 12:42:15 +02:00
|
|
|
else:
|
|
|
|
return (1, 2)
|
2023-10-17 22:16:01 +02:00
|
|
|
elif color == Black:
|
2023-10-17 12:42:15 +02:00
|
|
|
if long:
|
2023-10-20 02:23:07 +02:00
|
|
|
return (-2, -1)
|
2023-10-17 12:42:15 +02:00
|
|
|
else:
|
2023-10-20 02:23:07 +02:00
|
|
|
return (-1, -2)
|
2023-10-17 12:42:15 +02:00
|
|
|
|
|
|
|
|
2023-10-17 22:16:01 +02:00
|
|
|
func topLeftKnightMove(color: PieceColor, long: bool = true): Location {.inline.} =
|
|
|
|
if color == White:
|
2023-10-17 12:42:15 +02:00
|
|
|
if long:
|
|
|
|
return (-2, -1)
|
|
|
|
else:
|
|
|
|
return (-1, -2)
|
2023-10-17 22:16:01 +02:00
|
|
|
elif color == Black:
|
2023-10-17 12:42:15 +02:00
|
|
|
if long:
|
|
|
|
return (2, 1)
|
|
|
|
else:
|
|
|
|
return (1, 2)
|
|
|
|
|
|
|
|
|
2023-10-17 22:16:01 +02:00
|
|
|
func topRightKnightMove(color: PieceColor, long: bool = true): Location {.inline.} =
|
|
|
|
if color == White:
|
2023-10-17 12:42:15 +02:00
|
|
|
if long:
|
|
|
|
return (-2, 1)
|
|
|
|
else:
|
|
|
|
return (-1, 2)
|
2023-10-17 22:16:01 +02:00
|
|
|
elif color == Black:
|
2023-10-17 12:42:15 +02:00
|
|
|
if long:
|
|
|
|
return (2, -1)
|
|
|
|
else:
|
2023-10-21 18:19:41 +02:00
|
|
|
return (-1, 2)
|
2023-10-16 22:14:58 +02:00
|
|
|
|
|
|
|
|
2023-10-18 10:45:54 +02:00
|
|
|
func getActiveColor*(self: ChessBoard): PieceColor {.inline.} =
|
2023-10-16 22:14:58 +02:00
|
|
|
## Returns the currently active color
|
|
|
|
## (turn of who has to move)
|
2023-10-17 15:08:46 +02:00
|
|
|
return self.position.turn
|
|
|
|
|
|
|
|
|
2023-10-18 10:45:54 +02:00
|
|
|
func getEnPassantTarget*(self: ChessBoard): Location =
|
2023-10-17 16:38:43 +02:00
|
|
|
## Returns the current en passant target square
|
|
|
|
return self.position.enPassantSquare.targetSquare
|
|
|
|
|
|
|
|
|
2023-10-18 10:45:54 +02:00
|
|
|
func getMoveCount*(self: ChessBoard): int =
|
2023-10-17 17:27:33 +02:00
|
|
|
## Returns the number of full moves that
|
|
|
|
## have been played
|
|
|
|
return self.position.fullMoveCount
|
|
|
|
|
|
|
|
|
2023-10-18 10:45:54 +02:00
|
|
|
func getHalfMoveCount*(self: ChessBoard): int {.inline.} =
|
2023-10-17 17:27:33 +02:00
|
|
|
## Returns the current number of half-moves
|
|
|
|
## since the last irreversible move
|
|
|
|
return self.position.halfMoveClock
|
|
|
|
|
|
|
|
|
2023-10-16 14:55:43 +02:00
|
|
|
func getStartRow(piece: Piece): int {.inline.} =
|
|
|
|
## Retrieves the starting row of
|
|
|
|
## the given piece inside our 8x8
|
|
|
|
## grid
|
|
|
|
case piece.color:
|
|
|
|
of None:
|
|
|
|
return -1
|
|
|
|
of White:
|
|
|
|
case piece.kind:
|
|
|
|
of Pawn:
|
|
|
|
return 6
|
|
|
|
else:
|
2023-10-17 22:16:01 +02:00
|
|
|
return 7
|
2023-10-16 14:55:43 +02:00
|
|
|
of Black:
|
|
|
|
case piece.kind:
|
|
|
|
of Pawn:
|
|
|
|
return 1
|
|
|
|
else:
|
|
|
|
return 0
|
2023-10-12 10:14:37 +02:00
|
|
|
|
|
|
|
|
2023-10-16 15:25:48 +02:00
|
|
|
func getLastRow(color: PieceColor): int {.inline.} =
|
|
|
|
## Retrieves the location of the last
|
|
|
|
## row relative to the given color
|
2023-10-17 10:31:38 +02:00
|
|
|
case color:
|
|
|
|
of White:
|
|
|
|
return 0
|
|
|
|
of Black:
|
|
|
|
return 7
|
|
|
|
else:
|
|
|
|
return -1
|
2023-10-16 15:25:48 +02:00
|
|
|
|
|
|
|
|
2023-10-12 11:55:12 +02:00
|
|
|
proc newChessboard: ChessBoard =
|
|
|
|
## Returns a new, empty chessboard
|
2023-03-18 18:14:30 +01:00
|
|
|
new(result)
|
|
|
|
# Turns our flat sequence into an 8x8 grid
|
|
|
|
result.grid = newMatrixFromSeq[Piece](empty, (8, 8))
|
2023-10-17 15:08:46 +02:00
|
|
|
result.position = Position(attacked: (@[], @[]),
|
|
|
|
enPassantSquare: emptyMove(),
|
|
|
|
move: emptyMove(),
|
2023-10-20 02:23:07 +02:00
|
|
|
turn: White,
|
|
|
|
fullMoveCount: 1)
|
2023-10-17 15:08:46 +02:00
|
|
|
|
2023-10-12 11:55:12 +02:00
|
|
|
|
|
|
|
|
|
|
|
proc newChessboardFromFEN*(state: string): ChessBoard =
|
|
|
|
## Initializes a chessboard with the
|
|
|
|
## state encoded by the given FEN string
|
|
|
|
result = newChessboard()
|
2023-03-18 18:14:30 +01:00
|
|
|
var
|
|
|
|
# Current location in the grid
|
2023-10-18 10:45:54 +02:00
|
|
|
row: int8 = 0
|
|
|
|
column: int8 = 0
|
2023-03-18 18:14:30 +01:00
|
|
|
# Current section in the FEN string
|
|
|
|
section = 0
|
|
|
|
# Current index into the FEN string
|
|
|
|
index = 0
|
2023-10-12 11:55:12 +02:00
|
|
|
# Temporary variable to store the piece
|
|
|
|
piece: Piece
|
2023-03-18 18:14:30 +01:00
|
|
|
# See https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation
|
|
|
|
while index <= state.high():
|
|
|
|
var c = state[index]
|
|
|
|
if c == ' ':
|
|
|
|
# Next section
|
|
|
|
inc(section)
|
|
|
|
inc(index)
|
|
|
|
continue
|
|
|
|
case section:
|
|
|
|
of 0:
|
|
|
|
# Piece placement data
|
|
|
|
case c.toLowerAscii():
|
2023-10-12 11:55:12 +02:00
|
|
|
# Piece
|
2023-03-18 18:14:30 +01:00
|
|
|
of 'r', 'n', 'b', 'q', 'k', 'p':
|
2023-10-12 11:55:12 +02:00
|
|
|
# We know for a fact these values are in our
|
|
|
|
# enumeration, so all is good
|
|
|
|
{.push.}
|
|
|
|
{.warning[HoleEnumConv]:off.}
|
|
|
|
piece = Piece(kind: PieceKind(c.toLowerAscii()), color: if c.isUpperAscii(): White else: Black)
|
|
|
|
{.pop.}
|
|
|
|
case piece.color:
|
|
|
|
of Black:
|
|
|
|
case piece.kind:
|
|
|
|
of Pawn:
|
2023-10-17 15:08:46 +02:00
|
|
|
result.position.pieces.black.pawns.add((row, column))
|
2023-10-12 11:55:12 +02:00
|
|
|
of Bishop:
|
2023-10-17 15:08:46 +02:00
|
|
|
result.position.pieces.black.bishops.add((row, column))
|
2023-10-12 11:55:12 +02:00
|
|
|
of Knight:
|
2023-10-17 15:08:46 +02:00
|
|
|
result.position.pieces.black.knights.add((row, column))
|
2023-10-12 11:55:12 +02:00
|
|
|
of Rook:
|
2023-10-17 15:08:46 +02:00
|
|
|
result.position.pieces.black.rooks.add((row, column))
|
2023-10-12 11:55:12 +02:00
|
|
|
of Queen:
|
2023-10-17 15:08:46 +02:00
|
|
|
result.position.pieces.black.queens.add((row, column))
|
2023-10-12 11:55:12 +02:00
|
|
|
of King:
|
2023-10-17 15:08:46 +02:00
|
|
|
result.position.pieces.black.king = (row, column)
|
2023-10-12 11:55:12 +02:00
|
|
|
else:
|
|
|
|
discard
|
|
|
|
of White:
|
|
|
|
case piece.kind:
|
|
|
|
of Pawn:
|
2023-10-17 15:08:46 +02:00
|
|
|
result.position.pieces.white.pawns.add((row, column))
|
2023-10-12 11:55:12 +02:00
|
|
|
of Bishop:
|
2023-10-17 15:08:46 +02:00
|
|
|
result.position.pieces.white.bishops.add((row, column))
|
2023-10-12 11:55:12 +02:00
|
|
|
of Knight:
|
2023-10-17 15:08:46 +02:00
|
|
|
result.position.pieces.white.knights.add((row, column))
|
2023-10-12 11:55:12 +02:00
|
|
|
of Rook:
|
2023-10-17 15:08:46 +02:00
|
|
|
result.position.pieces.white.rooks.add((row, column))
|
2023-10-12 11:55:12 +02:00
|
|
|
of Queen:
|
2023-10-17 15:08:46 +02:00
|
|
|
result.position.pieces.white.queens.add((row, column))
|
2023-10-12 11:55:12 +02:00
|
|
|
of King:
|
2023-10-17 15:08:46 +02:00
|
|
|
result.position.pieces.white.king = (row, column)
|
2023-10-12 11:55:12 +02:00
|
|
|
else:
|
|
|
|
discard
|
|
|
|
else:
|
|
|
|
discard
|
|
|
|
result.grid[row, column] = piece
|
2023-03-18 18:14:30 +01:00
|
|
|
inc(column)
|
|
|
|
of '/':
|
2023-10-12 11:55:12 +02:00
|
|
|
# Next row
|
2023-03-18 18:14:30 +01:00
|
|
|
inc(row)
|
|
|
|
column = 0
|
|
|
|
of '0'..'9':
|
2023-10-12 11:55:12 +02:00
|
|
|
# Skip x columns
|
2023-10-20 02:23:07 +02:00
|
|
|
let x = int(uint8(c) - uint8('0'))
|
|
|
|
if x > 8:
|
2023-03-18 18:14:30 +01:00
|
|
|
raise newException(ValueError, "invalid skip value (> 8) in FEN string")
|
2023-10-18 10:45:54 +02:00
|
|
|
column += int8(x)
|
2023-03-18 18:14:30 +01:00
|
|
|
else:
|
|
|
|
raise newException(ValueError, "invalid piece identifier in FEN string")
|
|
|
|
of 1:
|
|
|
|
# Active color
|
|
|
|
case c:
|
|
|
|
of 'w':
|
2023-10-17 15:08:46 +02:00
|
|
|
result.position.turn = White
|
2023-03-18 18:14:30 +01:00
|
|
|
of 'b':
|
2023-10-17 15:08:46 +02:00
|
|
|
result.position.turn = Black
|
2023-03-18 18:14:30 +01:00
|
|
|
else:
|
2023-10-12 10:14:37 +02:00
|
|
|
raise newException(ValueError, "invalid active color identifier in FEN string")
|
2023-03-18 18:14:30 +01:00
|
|
|
of 2:
|
|
|
|
# Castling availability
|
|
|
|
case c:
|
|
|
|
of '-':
|
|
|
|
# Neither side can castle anywhere: do nothing,
|
|
|
|
# as the castling metadata is set to this state
|
|
|
|
# by default
|
|
|
|
discard
|
|
|
|
of 'K':
|
2023-10-17 22:16:01 +02:00
|
|
|
result.position.castlingAvailable.white.king = true
|
2023-03-18 18:14:30 +01:00
|
|
|
of 'Q':
|
2023-10-17 22:16:01 +02:00
|
|
|
result.position.castlingAvailable.white.queen = true
|
2023-03-18 18:14:30 +01:00
|
|
|
of 'k':
|
2023-10-17 22:16:01 +02:00
|
|
|
result.position.castlingAvailable.black.king = true
|
2023-03-18 18:14:30 +01:00
|
|
|
of 'q':
|
2023-10-17 22:16:01 +02:00
|
|
|
result.position.castlingAvailable.black.queen = true
|
2023-03-18 18:14:30 +01:00
|
|
|
else:
|
|
|
|
raise newException(ValueError, "invalid castling availability in FEN string")
|
|
|
|
of 3:
|
|
|
|
# En passant target square
|
|
|
|
case c:
|
|
|
|
of '-':
|
2023-10-12 11:55:12 +02:00
|
|
|
# Field is already uninitialized to the correct state
|
|
|
|
discard
|
2023-03-18 18:14:30 +01:00
|
|
|
else:
|
2023-10-17 16:38:43 +02:00
|
|
|
result.position.enPassantSquare.targetSquare = state[index..index+1].algebraicToLocation()
|
2023-10-17 15:08:46 +02:00
|
|
|
# Just for cleanliness purposes, we fill in the other metadata as
|
2023-03-18 18:14:30 +01:00
|
|
|
# well
|
2023-10-17 15:08:46 +02:00
|
|
|
result.position.enPassantSquare.piece.color = result.getActiveColor()
|
|
|
|
result.position.enPassantSquare.piece.kind = Pawn
|
2023-03-18 18:14:30 +01:00
|
|
|
# Square metadata is 2 bytes long
|
|
|
|
inc(index)
|
|
|
|
of 4:
|
|
|
|
# Halfmove clock
|
|
|
|
var s = ""
|
|
|
|
while not state[index].isSpaceAscii():
|
|
|
|
s.add(state[index])
|
|
|
|
inc(index)
|
|
|
|
# Backtrack so the space is seen by the
|
|
|
|
# next iteration of the loop
|
|
|
|
dec(index)
|
2023-10-18 10:45:54 +02:00
|
|
|
result.position.halfMoveClock = parseInt(s).int8
|
2023-03-18 18:14:30 +01:00
|
|
|
of 5:
|
|
|
|
# Fullmove number
|
|
|
|
var s = ""
|
|
|
|
while index <= state.high():
|
|
|
|
s.add(state[index])
|
|
|
|
inc(index)
|
2023-10-18 10:45:54 +02:00
|
|
|
result.position.fullMoveCount = parseInt(s).int8
|
2023-03-18 18:14:30 +01:00
|
|
|
else:
|
|
|
|
raise newException(ValueError, "too many fields in FEN string")
|
|
|
|
inc(index)
|
2023-10-21 18:19:41 +02:00
|
|
|
result.updateAttackedSquares()
|
|
|
|
|
2023-03-18 18:14:30 +01:00
|
|
|
|
2023-10-12 10:14:37 +02:00
|
|
|
|
|
|
|
proc newDefaultChessboard*: ChessBoard =
|
|
|
|
## Initializes a chessboard with the
|
|
|
|
## starting position
|
2023-10-13 11:54:57 +02:00
|
|
|
return newChessboardFromFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
|
|
|
|
|
|
|
|
|
2023-10-15 16:53:44 +02:00
|
|
|
proc countPieces*(self: ChessBoard, kind: PieceKind, color: PieceColor): int =
|
|
|
|
## Counts the number of pieces with
|
|
|
|
## the given color and type
|
|
|
|
case color:
|
|
|
|
of White:
|
|
|
|
case kind:
|
|
|
|
of Pawn:
|
2023-10-17 15:08:46 +02:00
|
|
|
return self.position.pieces.white.pawns.len()
|
2023-10-15 16:53:44 +02:00
|
|
|
of Bishop:
|
2023-10-17 15:08:46 +02:00
|
|
|
return self.position.pieces.white.bishops.len()
|
2023-10-15 16:53:44 +02:00
|
|
|
of Knight:
|
2023-10-17 15:08:46 +02:00
|
|
|
return self.position.pieces.white.knights.len()
|
2023-10-15 16:53:44 +02:00
|
|
|
of Rook:
|
2023-10-17 15:08:46 +02:00
|
|
|
return self.position.pieces.white.rooks.len()
|
2023-10-15 16:53:44 +02:00
|
|
|
of Queen:
|
2023-10-17 15:08:46 +02:00
|
|
|
return self.position.pieces.white.queens.len()
|
2023-10-15 16:53:44 +02:00
|
|
|
of King:
|
|
|
|
# There shall be only one, forever
|
|
|
|
return 1
|
|
|
|
else:
|
|
|
|
discard
|
|
|
|
of Black:
|
|
|
|
case kind:
|
|
|
|
of Pawn:
|
2023-10-17 15:08:46 +02:00
|
|
|
return self.position.pieces.black.pawns.len()
|
2023-10-15 16:53:44 +02:00
|
|
|
of Bishop:
|
2023-10-17 15:08:46 +02:00
|
|
|
return self.position.pieces.black.bishops.len()
|
2023-10-15 16:53:44 +02:00
|
|
|
of Knight:
|
2023-10-17 15:08:46 +02:00
|
|
|
return self.position.pieces.black.knights.len()
|
2023-10-15 16:53:44 +02:00
|
|
|
of Rook:
|
2023-10-17 15:08:46 +02:00
|
|
|
return self.position.pieces.black.rooks.len()
|
2023-10-15 16:53:44 +02:00
|
|
|
of Queen:
|
2023-10-17 15:08:46 +02:00
|
|
|
return self.position.pieces.black.queens.len()
|
2023-10-15 16:53:44 +02:00
|
|
|
of King:
|
2023-10-16 14:55:43 +02:00
|
|
|
# In perpetuity
|
2023-10-15 16:53:44 +02:00
|
|
|
return 1
|
|
|
|
else:
|
|
|
|
discard
|
|
|
|
of None:
|
|
|
|
raise newException(ValueError, "invalid piece type")
|
|
|
|
|
|
|
|
|
|
|
|
proc countPieces*(self: ChessBoard, piece: Piece): int =
|
|
|
|
## Returns the number of pieces on the board that
|
|
|
|
## are of the same type and color of the given piece
|
|
|
|
return self.countPieces(piece.kind, piece.color)
|
|
|
|
|
|
|
|
|
2023-10-18 10:45:54 +02:00
|
|
|
func rankToColumn(rank: int): int8 =
|
2023-10-15 16:53:44 +02:00
|
|
|
## Converts a chess rank (1-indexed)
|
|
|
|
## into a 0-indexed column value for our
|
|
|
|
## board. This converter is necessary because
|
|
|
|
## chess positions are indexed differently with
|
|
|
|
## respect to our internal representation
|
2023-10-18 10:45:54 +02:00
|
|
|
const indeces: array[8, int8] = [7, 6, 5, 4, 3, 2, 1, 0]
|
2023-10-15 16:53:44 +02:00
|
|
|
return indeces[rank - 1]
|
|
|
|
|
|
|
|
|
2023-10-17 17:27:33 +02:00
|
|
|
func rowToRank(row: int): int =
|
|
|
|
const indeces = [8, 7, 6, 5, 4, 3, 2, 1]
|
|
|
|
return indeces[row]
|
|
|
|
|
|
|
|
|
2023-10-17 16:38:43 +02:00
|
|
|
proc algebraicToLocation*(s: string): Location =
|
2023-10-15 16:53:44 +02:00
|
|
|
## Converts a square location from algebraic
|
|
|
|
## notation to its corresponding row and column
|
|
|
|
## in the chess grid (0 indexed)
|
|
|
|
if len(s) != 2:
|
|
|
|
raise newException(ValueError, "algebraic position must be of length 2")
|
|
|
|
|
|
|
|
var s = s.toLowerAscii()
|
|
|
|
if s[0] notin 'a'..'h':
|
|
|
|
raise newException(ValueError, &"algebraic position has invalid first character ('{s[0]}')")
|
|
|
|
if s[1] notin '1'..'8':
|
|
|
|
raise newException(ValueError, &"algebraic position has invalid second character ('{s[1]}')")
|
|
|
|
|
2023-10-18 10:45:54 +02:00
|
|
|
let rank = int8(uint8(s[0]) - uint8('a'))
|
2023-10-17 17:27:33 +02:00
|
|
|
# Convert the file character to a number
|
2023-10-18 10:45:54 +02:00
|
|
|
let file = rankToColumn(int8(uint8(s[1]) - uint8('0')))
|
2023-10-17 17:27:33 +02:00
|
|
|
return (file, rank)
|
2023-10-15 16:53:44 +02:00
|
|
|
|
|
|
|
|
2023-10-17 22:16:01 +02:00
|
|
|
func locationToAlgebraic*(loc: Location): string {.inline.} =
|
2023-10-17 16:38:43 +02:00
|
|
|
## Converts a location from our internal row, column
|
|
|
|
## notation to a square in algebraic notation
|
2023-10-17 17:27:33 +02:00
|
|
|
return &"{char(uint8(loc.col) + uint8('a'))}{rowToRank(loc.row)}"
|
2023-10-17 16:38:43 +02:00
|
|
|
|
|
|
|
|
2023-10-21 18:19:41 +02:00
|
|
|
func getPiece*(self: ChessBoard, loc: Location): Piece =
|
|
|
|
## Gets the piece at the given location
|
|
|
|
return self.grid[loc.row, loc.col]
|
|
|
|
|
|
|
|
|
2023-10-17 22:16:01 +02:00
|
|
|
func getPiece*(self: ChessBoard, square: string): Piece =
|
2023-10-15 16:53:44 +02:00
|
|
|
## Gets the piece on the given square
|
|
|
|
## in algebraic notation
|
2023-10-21 18:19:41 +02:00
|
|
|
return self.getPiece(square.algebraicToLocation())
|
2023-10-15 16:53:44 +02:00
|
|
|
|
2023-10-16 22:14:58 +02:00
|
|
|
|
|
|
|
|
2023-10-17 22:16:01 +02:00
|
|
|
func isCapture*(self: ChessBoard, move: Move): bool {.inline.} =
|
2023-10-16 22:14:58 +02:00
|
|
|
## Returns whether the given move is a capture
|
|
|
|
## or not
|
2023-10-21 18:19:41 +02:00
|
|
|
return move.flag in [Capture, EnPassant]
|
|
|
|
|
|
|
|
|
|
|
|
func getCapture*(self: ChessBoard, move: Move): Location =
|
|
|
|
## Returns the location that would be captured if this
|
|
|
|
## move were played on the board
|
|
|
|
if not self.isCapture(move):
|
|
|
|
return emptyLocation()
|
|
|
|
return move.targetSquare
|
2023-10-16 22:14:58 +02:00
|
|
|
|
|
|
|
|
2023-10-17 22:16:01 +02:00
|
|
|
proc inCheck*(self: ChessBoard, color: PieceColor = None): bool =
|
|
|
|
## Returns whether the given color's
|
|
|
|
## king is in check. If the color is
|
|
|
|
## set to None, checks are checked
|
|
|
|
## for the active color's king
|
2023-10-20 02:23:07 +02:00
|
|
|
var color = color
|
|
|
|
if color == None:
|
|
|
|
color = self.getActiveColor()
|
2023-10-17 22:16:01 +02:00
|
|
|
case color:
|
|
|
|
of White:
|
|
|
|
return self.isAttacked(self.position.pieces.white.king)
|
|
|
|
of Black:
|
|
|
|
return self.isAttacked(self.position.pieces.black.king)
|
2023-10-20 02:23:07 +02:00
|
|
|
else:
|
|
|
|
# Unreachable
|
|
|
|
discard
|
2023-10-17 22:16:01 +02:00
|
|
|
|
|
|
|
|
|
|
|
proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: bool] {.inline.} =
|
|
|
|
## Returns the sides on which castling is allowed
|
|
|
|
## for the given color. If the color is Empty, the
|
|
|
|
## currently active color is used
|
|
|
|
var color = color
|
|
|
|
if color == None:
|
|
|
|
color = self.getActiveColor()
|
|
|
|
# If the rooks or king have been moved, castling
|
|
|
|
# rights have been lost
|
|
|
|
case color:
|
|
|
|
of White:
|
|
|
|
result.king = self.position.castlingAvailable.white.king
|
|
|
|
result.queen = self.position.castlingAvailable.white.queen
|
|
|
|
of Black:
|
|
|
|
result.king = self.position.castlingAvailable.black.king
|
|
|
|
result.queen = self.position.castlingAvailable.black.queen
|
|
|
|
of None:
|
|
|
|
# Unreachable
|
|
|
|
discard
|
|
|
|
var
|
|
|
|
loc: Location
|
|
|
|
queenSide: Location
|
|
|
|
kingSide: Location
|
|
|
|
# If the path between the king and a rook is blocked, then castling
|
|
|
|
# is prohibited on that side
|
|
|
|
case color:
|
|
|
|
of White:
|
|
|
|
loc = self.position.pieces.white.king
|
|
|
|
queenSide = color.leftSide()
|
|
|
|
kingSide = color.rightSide()
|
|
|
|
of Black:
|
|
|
|
loc = self.position.pieces.black.king
|
|
|
|
queenSide = color.rightSide()
|
|
|
|
kingSide = color.leftSide()
|
|
|
|
of None:
|
|
|
|
# Unreachable
|
|
|
|
discard
|
|
|
|
|
|
|
|
if result.king:
|
|
|
|
# Short castle
|
|
|
|
var location = loc
|
|
|
|
while true:
|
|
|
|
location = location + kingSide
|
|
|
|
if not location.isValid():
|
|
|
|
break
|
|
|
|
if self.grid[location.row, location.col].kind == Empty:
|
|
|
|
continue
|
|
|
|
if location == color.kingSideRook() and self.grid[location.row, location.col].kind == Rook:
|
|
|
|
break
|
|
|
|
# Blocked by a piece
|
|
|
|
result.king = false
|
|
|
|
break
|
|
|
|
|
|
|
|
if result.queen:
|
|
|
|
# Long castle
|
|
|
|
var location = loc
|
|
|
|
while true:
|
|
|
|
location = location + queenSide
|
|
|
|
if not location.isValid():
|
|
|
|
break
|
|
|
|
if self.grid[location.row, location.col].kind == Empty:
|
|
|
|
continue
|
|
|
|
if location == color.queenSideRook() and self.grid[location.row, location.col].kind == Rook:
|
|
|
|
break
|
|
|
|
# Blocked by a piece
|
|
|
|
result.queen = false
|
|
|
|
break
|
|
|
|
|
|
|
|
# If the castling king would walk into, through or out of check
|
|
|
|
# while castling on a given side, then it is not possible to castle
|
|
|
|
# on that side until the threat exists
|
|
|
|
|
|
|
|
if (result.king or result.queen) and self.inCheck(color):
|
|
|
|
# Only check for checks if castling is still available
|
|
|
|
# by this point (if we can avoid calls to generateMoves,
|
|
|
|
# we should)
|
|
|
|
return
|
|
|
|
# TODO: Check for attacks in the various other squares
|
|
|
|
|
|
|
|
|
2023-10-16 14:55:43 +02:00
|
|
|
proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
|
2023-10-16 15:25:48 +02:00
|
|
|
## Generates the possible moves for the pawn in the given
|
|
|
|
## location
|
2023-10-16 14:55:43 +02:00
|
|
|
var
|
|
|
|
piece = self.grid[location.row, location.col]
|
2023-10-16 15:25:48 +02:00
|
|
|
locations: seq[Location] = @[]
|
2023-10-20 02:23:07 +02:00
|
|
|
flags: seq[MoveFlag] = @[]
|
2023-10-16 14:55:43 +02:00
|
|
|
doAssert piece.kind == Pawn, &"generatePawnMoves called on a {piece.kind}"
|
2023-10-16 22:14:58 +02:00
|
|
|
# Pawns can move forward one square
|
2023-10-17 22:16:01 +02:00
|
|
|
let forwardOffset = piece.color.forward()
|
2023-10-16 22:14:58 +02:00
|
|
|
let forward = (forwardOffset + location)
|
2023-10-20 02:23:07 +02:00
|
|
|
# Only if the square is empty though
|
2023-10-16 22:14:58 +02:00
|
|
|
if forward.isValid() and self.grid[forward.row, forward.col].color == None:
|
|
|
|
locations.add(forwardOffset)
|
2023-10-20 02:23:07 +02:00
|
|
|
flags.add(Default)
|
2023-10-16 22:14:58 +02:00
|
|
|
# If the pawn is on its first rank, it can push two squares
|
|
|
|
if location.row == piece.getStartRow():
|
2023-10-20 02:23:07 +02:00
|
|
|
let doubleOffset = piece.color.doublePush()
|
|
|
|
let double = location + doubleOffset
|
|
|
|
# Check if both squares are available
|
|
|
|
if double.isValid() and self.grid[forward.row, forward.col].color == None and self.grid[double.row, double.col].color == None:
|
|
|
|
locations.add(piece.color.doublePush())
|
|
|
|
flags.add(DoublePush)
|
2023-10-17 15:08:46 +02:00
|
|
|
if self.position.enPassantSquare.piece.color == piece.color.opposite:
|
|
|
|
if abs(self.position.enPassantSquare.targetSquare.col - location.col) == 1 and abs(self.position.enPassantSquare.targetSquare.row - location.row) == 1:
|
2023-10-16 22:14:58 +02:00
|
|
|
# Only viable if the piece is on the diagonal of the target
|
2023-10-17 15:08:46 +02:00
|
|
|
locations.add(self.position.enPassantSquare.targetSquare)
|
2023-10-20 02:23:07 +02:00
|
|
|
flags.add(EnPassant)
|
2023-10-16 22:14:58 +02:00
|
|
|
# They can also move on either diagonal one
|
|
|
|
# square, but only to capture
|
2023-10-20 02:23:07 +02:00
|
|
|
var diagonal = piece.color.topRightDiagonal()
|
|
|
|
if (diagonal + location).isValid() and self.isCapture(Move(piece: piece, startSquare: location, targetSquare: location + diagonal)):
|
|
|
|
locations.add(diagonal)
|
|
|
|
flags.add(Capture)
|
|
|
|
diagonal = piece.color.topLeftDiagonal()
|
|
|
|
if (diagonal + location).isValid() and self.isCapture(Move(piece: piece, startSquare: location, targetSquare: location + diagonal)):
|
|
|
|
locations.add(diagonal)
|
|
|
|
flags.add(Capture)
|
|
|
|
|
2023-10-16 22:14:58 +02:00
|
|
|
var
|
|
|
|
newLocation: Location
|
|
|
|
targetPiece: Piece
|
2023-10-20 02:23:07 +02:00
|
|
|
for (target, flag) in zip(locations, flags):
|
2023-10-16 22:14:58 +02:00
|
|
|
newLocation = location + target
|
|
|
|
targetPiece = self.grid[newLocation.row, newLocation.col]
|
|
|
|
if newLocation.row == piece.color.getLastRow():
|
2023-10-20 02:23:07 +02:00
|
|
|
# Pawn reached the other side of the board: generate all potential piece promotions
|
2023-10-16 15:25:48 +02:00
|
|
|
for promotionType in [PromoteToKnight, PromoteToBishop, PromoteToRook, PromoteToQueen]:
|
2023-10-16 22:14:58 +02:00
|
|
|
result.add(Move(startSquare: location, targetSquare: newLocation, piece: piece, flag: promotionType))
|
|
|
|
continue
|
2023-10-20 02:23:07 +02:00
|
|
|
result.add(Move(startSquare: location, targetSquare: newLocation, piece: piece, flag: flag))
|
2023-10-17 16:38:43 +02:00
|
|
|
|
2023-10-16 14:55:43 +02:00
|
|
|
|
|
|
|
proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
|
2023-10-16 22:14:58 +02:00
|
|
|
## Generates moves for the sliding piece in the given location
|
2023-10-16 14:55:43 +02:00
|
|
|
var
|
|
|
|
piece = self.grid[location.row, location.col]
|
|
|
|
doAssert piece.kind in [Bishop, Rook, Queen], &"generateSlidingMoves called on a {piece.kind}"
|
2023-10-16 23:02:58 +02:00
|
|
|
var directions: seq[Location] = @[]
|
|
|
|
# Only check in the right directions for the chosen piece
|
|
|
|
if piece.kind in [Bishop, Queen]:
|
2023-10-17 22:16:01 +02:00
|
|
|
directions.add(piece.color.topLeftDiagonal())
|
|
|
|
directions.add(piece.color.topRightDiagonal())
|
|
|
|
directions.add(piece.color.bottomLeftDiagonal())
|
|
|
|
directions.add(piece.color.bottomRightDiagonal())
|
2023-10-16 23:02:58 +02:00
|
|
|
if piece.kind in [Queen, Rook]:
|
2023-10-17 22:16:01 +02:00
|
|
|
directions.add(piece.color.topSide())
|
|
|
|
directions.add(piece.color.bottomSide())
|
|
|
|
directions.add(piece.color.rightSide())
|
|
|
|
directions.add(piece.color.leftSide())
|
2023-10-16 22:14:58 +02:00
|
|
|
for direction in directions:
|
|
|
|
# Slide in this direction as long as it's possible
|
|
|
|
var
|
|
|
|
square: Location = location
|
|
|
|
otherPiece: Piece
|
|
|
|
while true:
|
|
|
|
square = square + direction
|
|
|
|
# End of board reached
|
|
|
|
if not square.isValid():
|
|
|
|
break
|
|
|
|
otherPiece = self.grid[square.row, square.col]
|
|
|
|
# A friendly piece is in the way
|
|
|
|
if otherPiece.color == piece.color:
|
|
|
|
break
|
2023-10-17 10:31:38 +02:00
|
|
|
if otherPiece.color == piece.color.opposite:
|
2023-10-17 12:08:07 +02:00
|
|
|
# Target square contains an enemy piece: capture
|
|
|
|
# it and stop going any further
|
2023-10-20 02:23:07 +02:00
|
|
|
result.add(Move(startSquare: location, targetSquare: square, piece: piece, flag: Capture))
|
2023-10-17 10:31:38 +02:00
|
|
|
break
|
|
|
|
# Target square is empty
|
2023-10-16 22:14:58 +02:00
|
|
|
result.add(Move(startSquare: location, targetSquare: square, piece: piece))
|
2023-10-16 14:55:43 +02:00
|
|
|
|
|
|
|
|
2023-10-17 12:08:07 +02:00
|
|
|
proc generateKingMoves(self: ChessBoard, location: Location): seq[Move] =
|
|
|
|
## Generates moves for the king in the given location
|
|
|
|
var
|
|
|
|
piece = self.grid[location.row, location.col]
|
|
|
|
doAssert piece.kind == King, &"generateKingMoves called on a {piece.kind}"
|
2023-10-17 22:16:01 +02:00
|
|
|
var directions: seq[Location] = @[piece.color.topLeftDiagonal(),
|
|
|
|
piece.color.topRightDiagonal(),
|
|
|
|
piece.color.bottomRightDiagonal(),
|
|
|
|
piece.color.bottomLeftDiagonal(),
|
|
|
|
piece.color.topSide(),
|
|
|
|
piece.color.bottomSide(),
|
|
|
|
piece.color.leftSide(),
|
|
|
|
piece.color.rightSide()]
|
|
|
|
# Castling
|
|
|
|
let canCastle = self.canCastle()
|
|
|
|
if canCastle.queen:
|
2023-10-20 02:23:07 +02:00
|
|
|
directions.add(longCastleKing())
|
2023-10-17 22:16:01 +02:00
|
|
|
if canCastle.king:
|
2023-10-20 02:23:07 +02:00
|
|
|
directions.add(shortCastleKing())
|
2023-10-17 22:16:01 +02:00
|
|
|
var flag = Default
|
2023-10-17 12:08:07 +02:00
|
|
|
for direction in directions:
|
2023-10-20 02:23:07 +02:00
|
|
|
if direction == longCastleKing():
|
2023-10-17 22:16:01 +02:00
|
|
|
flag = CastleLong
|
2023-10-20 02:23:07 +02:00
|
|
|
elif direction == shortCastleKing():
|
2023-10-17 22:16:01 +02:00
|
|
|
flag = CastleShort
|
2023-10-20 02:23:07 +02:00
|
|
|
else:
|
|
|
|
flag = Default
|
2023-10-17 12:08:07 +02:00
|
|
|
# Step in this direction once
|
|
|
|
let square: Location = location + direction
|
|
|
|
# End of board reached
|
|
|
|
if not square.isValid():
|
|
|
|
continue
|
|
|
|
let otherPiece = self.grid[square.row, square.col]
|
2023-10-20 02:23:07 +02:00
|
|
|
if otherPiece.color == self.getActiveColor.opposite():
|
|
|
|
flag = Capture
|
2023-10-17 22:16:01 +02:00
|
|
|
# A friendly piece is in the way, move onto the next
|
|
|
|
# direction
|
2023-10-17 12:08:07 +02:00
|
|
|
if otherPiece.color == piece.color:
|
|
|
|
continue
|
2023-10-17 22:16:01 +02:00
|
|
|
# Target square is empty or contains an enemy piece:
|
|
|
|
# All good for us!
|
|
|
|
result.add(Move(startSquare: location, targetSquare: square, piece: piece, flag: flag))
|
2023-10-17 12:42:15 +02:00
|
|
|
|
|
|
|
|
|
|
|
proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] =
|
|
|
|
## Generates moves for the knight in the given location
|
|
|
|
var
|
|
|
|
piece = self.grid[location.row, location.col]
|
|
|
|
doAssert piece.kind == Knight, &"generateKnightMoves called on a {piece.kind}"
|
2023-10-17 22:16:01 +02:00
|
|
|
var directions: seq[Location] = @[piece.color.bottomLeftKnightMove(),
|
|
|
|
piece.color.bottomRightKnightMove(),
|
|
|
|
piece.color.topLeftKnightMove(),
|
|
|
|
piece.color.topRightKnightMove(),
|
|
|
|
piece.color.bottomLeftKnightMove(long=false),
|
|
|
|
piece.color.bottomRightKnightMove(long=false),
|
|
|
|
piece.color.topLeftKnightMove(long=false),
|
|
|
|
piece.color.topRightKnightMove(long=false)]
|
2023-10-17 12:42:15 +02:00
|
|
|
for direction in directions:
|
|
|
|
# Jump to this square
|
|
|
|
let square: Location = location + direction
|
|
|
|
# End of board reached
|
|
|
|
if not square.isValid():
|
|
|
|
continue
|
|
|
|
let otherPiece = self.grid[square.row, square.col]
|
|
|
|
# A friendly piece is in the way
|
|
|
|
if otherPiece.color == piece.color:
|
|
|
|
continue
|
|
|
|
if otherPiece.color == piece.color.opposite:
|
|
|
|
# Target square contains an enemy piece: capture
|
|
|
|
# it
|
2023-10-20 02:23:07 +02:00
|
|
|
result.add(Move(startSquare: location, targetSquare: square, piece: piece, flag: Capture))
|
2023-10-17 12:42:15 +02:00
|
|
|
continue
|
|
|
|
# Target square is empty
|
|
|
|
result.add(Move(startSquare: location, targetSquare: square, piece: piece))
|
|
|
|
|
2023-10-17 12:08:07 +02:00
|
|
|
|
2023-10-16 14:55:43 +02:00
|
|
|
proc generateMoves(self: ChessBoard, location: Location): seq[Move] =
|
2023-10-17 22:16:01 +02:00
|
|
|
## Returns the list of possible pseudo-legal chess moves for the
|
2023-10-16 14:55:43 +02:00
|
|
|
## piece in the given location
|
|
|
|
let piece = self.grid[location.row, location.col]
|
2023-10-16 15:25:48 +02:00
|
|
|
case piece.kind:
|
|
|
|
of Queen, Bishop, Rook:
|
|
|
|
return self.generateSlidingMoves(location)
|
|
|
|
of Pawn:
|
|
|
|
return self.generatePawnMoves(location)
|
2023-10-16 22:14:58 +02:00
|
|
|
of King:
|
2023-10-17 12:08:07 +02:00
|
|
|
return self.generateKingMoves(location)
|
2023-10-17 12:42:15 +02:00
|
|
|
of Knight:
|
|
|
|
return self.generateKnightMoves(location)
|
2023-10-15 16:53:44 +02:00
|
|
|
else:
|
2023-10-17 12:08:07 +02:00
|
|
|
return @[]
|
2023-10-15 16:53:44 +02:00
|
|
|
|
|
|
|
|
2023-10-18 10:45:54 +02:00
|
|
|
proc generateAllMoves*(self: ChessBoard): seq[Move] =
|
|
|
|
## Returns the list of all possible pseudo-legal moves
|
|
|
|
## in the current position
|
|
|
|
for i, row in self.grid:
|
|
|
|
for j, piece in row:
|
2023-10-21 18:19:41 +02:00
|
|
|
if self.grid[i, j].color == self.getActiveColor():
|
|
|
|
for move in self.generateMoves((int8(i), int8(j))):
|
|
|
|
result.add(move)
|
2023-10-18 10:45:54 +02:00
|
|
|
|
|
|
|
|
2023-10-21 18:19:41 +02:00
|
|
|
proc getAttackers*(self: ChessBoard, square: Location): seq[Location] =
|
2023-10-17 10:31:38 +02:00
|
|
|
## Returns all the attackers of the given square
|
2023-10-21 18:19:41 +02:00
|
|
|
for attack in self.position.attacked.black:
|
|
|
|
if attack.dest == square:
|
|
|
|
result.add(attack.source)
|
|
|
|
for attack in self.position.attacked.white:
|
|
|
|
if attack.dest == square:
|
|
|
|
result.add(attack.source)
|
2023-10-17 10:31:38 +02:00
|
|
|
|
|
|
|
# We don't use getAttackers because this one only cares about whether
|
|
|
|
# the square is attacked or not (and can therefore exit earlier than
|
|
|
|
# getAttackers)
|
|
|
|
proc isAttacked*(self: ChessBoard, loc: Location): bool =
|
|
|
|
## Returns whether the given location is attacked
|
2023-10-17 22:16:01 +02:00
|
|
|
## by the current opponent
|
2023-10-17 10:31:38 +02:00
|
|
|
let piece = self.grid[loc.row, loc.col]
|
|
|
|
case piece.color:
|
|
|
|
of White:
|
2023-10-21 18:19:41 +02:00
|
|
|
for attack in self.position.attacked.black:
|
|
|
|
if attack.dest == loc:
|
2023-10-17 10:31:38 +02:00
|
|
|
return true
|
|
|
|
of Black:
|
2023-10-21 18:19:41 +02:00
|
|
|
for attack in self.position.attacked.black:
|
|
|
|
if attack.dest == loc:
|
2023-10-17 10:31:38 +02:00
|
|
|
return true
|
|
|
|
of None:
|
2023-10-17 15:08:46 +02:00
|
|
|
case self.getActiveColor():
|
2023-10-17 10:31:38 +02:00
|
|
|
of White:
|
2023-10-21 18:19:41 +02:00
|
|
|
for attack in self.position.attacked.black:
|
|
|
|
if attack.dest == loc:
|
2023-10-17 10:31:38 +02:00
|
|
|
return true
|
|
|
|
of Black:
|
2023-10-21 18:19:41 +02:00
|
|
|
for attack in self.position.attacked.black:
|
|
|
|
if attack.dest == loc:
|
2023-10-17 10:31:38 +02:00
|
|
|
return true
|
|
|
|
else:
|
|
|
|
discard
|
|
|
|
|
|
|
|
|
|
|
|
proc isAttacked*(self: ChessBoard, square: string): bool =
|
|
|
|
## Returns whether the given square is attacked
|
2023-10-17 12:08:07 +02:00
|
|
|
## by its opponent
|
2023-10-17 16:38:43 +02:00
|
|
|
return self.isAttacked(square.algebraicToLocation())
|
2023-10-17 10:31:38 +02:00
|
|
|
|
|
|
|
|
2023-10-21 18:19:41 +02:00
|
|
|
proc updatePawnAttacks(self: ChessBoard) =
|
|
|
|
## Internal helper of updateAttackedSquares
|
|
|
|
for loc in self.position.pieces.white.pawns:
|
|
|
|
# Pawns are special in how they capture (i.e. the
|
|
|
|
# squares they can move to do not match the squares
|
|
|
|
# they can capture on. Sneaky fucks)
|
|
|
|
let piece = self.grid[loc.row, loc.col]
|
|
|
|
self.position.attacked.white.add((loc, loc + piece.color.topRightDiagonal()))
|
|
|
|
self.position.attacked.white.add((loc, loc + piece.color.topLeftDiagonal()))
|
|
|
|
# We do the same thing for black
|
|
|
|
for loc in self.position.pieces.black.pawns:
|
|
|
|
let piece = self.grid[loc.row, loc.col]
|
|
|
|
self.position.attacked.black.add((loc, loc + piece.color.topRightDiagonal()))
|
|
|
|
self.position.attacked.black.add((loc, loc + piece.color.topLeftDiagonal()))
|
2023-10-20 02:23:07 +02:00
|
|
|
|
2023-10-16 14:55:43 +02:00
|
|
|
|
2023-10-21 18:19:41 +02:00
|
|
|
proc getSlidingAttacks(self: ChessBoard, loc: Location): Attacked =
|
|
|
|
## Internal helper of updateSlidingAttacks
|
|
|
|
var
|
|
|
|
directions: seq[Location] = @[]
|
|
|
|
square: Location = loc
|
|
|
|
otherPiece: Piece
|
|
|
|
let piece = self.grid[loc.row, loc.col]
|
|
|
|
if piece.kind in [Bishop, Queen]:
|
|
|
|
directions.add(piece.color.topLeftDiagonal())
|
|
|
|
directions.add(piece.color.topRightDiagonal())
|
|
|
|
directions.add(piece.color.bottomLeftDiagonal())
|
|
|
|
directions.add(piece.color.bottomRightDiagonal())
|
|
|
|
if piece.kind in [Queen, Rook]:
|
|
|
|
directions.add(piece.color.topSide())
|
|
|
|
directions.add(piece.color.bottomSide())
|
|
|
|
directions.add(piece.color.rightSide())
|
|
|
|
directions.add(piece.color.leftSide())
|
|
|
|
for direction in directions:
|
|
|
|
square = loc
|
|
|
|
# Slide in this direction as long as it's possible
|
|
|
|
while true:
|
|
|
|
square = square + direction
|
|
|
|
# End of board reached
|
|
|
|
if not square.isValid():
|
|
|
|
break
|
|
|
|
otherPiece = self.grid[square.row, square.col]
|
|
|
|
# A piece is in the way: we cannot proceed
|
|
|
|
# any further
|
|
|
|
if otherPiece.color notin [piece.color.opposite(), None]:
|
|
|
|
break
|
|
|
|
# Target square is attacked
|
|
|
|
result.add((loc, square))
|
|
|
|
|
|
|
|
|
|
|
|
proc updateSlidingAttacks(self: ChessBoard) =
|
|
|
|
## Internal helper of updateAttackedSquares
|
|
|
|
|
|
|
|
var
|
|
|
|
directions: seq[Location]
|
|
|
|
piece: Piece
|
2023-10-17 12:08:07 +02:00
|
|
|
# Bishops
|
2023-10-17 15:08:46 +02:00
|
|
|
for loc in self.position.pieces.white.bishops:
|
2023-10-21 18:19:41 +02:00
|
|
|
for attack in self.getSlidingAttacks(loc):
|
|
|
|
self.position.attacked.white.add(attack)
|
|
|
|
for loc in self.position.pieces.black.bishops:
|
|
|
|
for attack in self.getSlidingAttacks(loc):
|
|
|
|
self.position.attacked.black.add(attack)
|
2023-10-17 15:08:46 +02:00
|
|
|
# Rooks
|
|
|
|
for loc in self.position.pieces.white.rooks:
|
2023-10-21 18:19:41 +02:00
|
|
|
for attack in self.getSlidingAttacks(loc):
|
|
|
|
self.position.attacked.white.add(attack)
|
|
|
|
for loc in self.position.pieces.black.rooks:
|
|
|
|
for attack in self.getSlidingAttacks(loc):
|
|
|
|
self.position.attacked.black.add(attack)
|
2023-10-17 12:08:07 +02:00
|
|
|
# Queens
|
2023-10-17 15:08:46 +02:00
|
|
|
for loc in self.position.pieces.white.queens:
|
2023-10-21 18:19:41 +02:00
|
|
|
for attack in self.getSlidingAttacks(loc):
|
|
|
|
self.position.attacked.white.add(attack)
|
|
|
|
for loc in self.position.pieces.black.queens:
|
|
|
|
for attack in self.getSlidingAttacks(loc):
|
|
|
|
self.position.attacked.black.add(attack)
|
|
|
|
|
|
|
|
|
|
|
|
proc updateAttackedSquares(self: ChessBoard) =
|
|
|
|
## Updates internal metadata about which squares
|
|
|
|
## are attacked. Called internally by doMove
|
|
|
|
|
|
|
|
self.position.attacked.white.setLen(0)
|
|
|
|
self.position.attacked.black.setLen(0)
|
|
|
|
# Pawns
|
|
|
|
self.updatePawnAttacks()
|
|
|
|
# Sliding pieces
|
|
|
|
self.updateSlidingAttacks()
|
|
|
|
# Knights
|
|
|
|
for loc in self.position.pieces.white.knights:
|
2023-10-16 23:02:58 +02:00
|
|
|
for move in self.generateMoves(loc):
|
2023-10-21 18:19:41 +02:00
|
|
|
self.position.attacked.white.add((move.startSquare, move.targetSquare))
|
2023-10-17 12:08:07 +02:00
|
|
|
# King
|
2023-10-17 15:08:46 +02:00
|
|
|
for move in self.generateMoves(self.position.pieces.white.king):
|
2023-10-21 18:19:41 +02:00
|
|
|
self.position.attacked.white.add((move.startSquare, move.targetSquare))
|
|
|
|
# Knights
|
2023-10-17 22:16:01 +02:00
|
|
|
for loc in self.position.pieces.black.knights:
|
|
|
|
for move in self.generateMoves(loc):
|
2023-10-21 18:19:41 +02:00
|
|
|
self.position.attacked.black.add((move.startSquare, move.targetSquare))
|
|
|
|
# King
|
2023-10-17 15:08:46 +02:00
|
|
|
for move in self.generateMoves(self.position.pieces.black.king):
|
2023-10-21 18:19:41 +02:00
|
|
|
self.position.attacked.black.add((move.startSquare, move.targetSquare))
|
2023-10-16 14:55:43 +02:00
|
|
|
|
|
|
|
|
2023-10-21 18:19:41 +02:00
|
|
|
proc removePiece(self: ChessBoard, location: Location, emptyGrid: bool = true) =
|
2023-10-15 16:53:44 +02:00
|
|
|
## Removes a piece from the board, updating necessary
|
|
|
|
## metadata
|
|
|
|
var piece = self.grid[location.row, location.col]
|
2023-10-21 18:19:41 +02:00
|
|
|
if emptyGrid:
|
|
|
|
self.grid[location.row, location.col] = emptyPiece()
|
2023-10-15 16:53:44 +02:00
|
|
|
case piece.color:
|
|
|
|
of White:
|
|
|
|
case piece.kind:
|
|
|
|
of Pawn:
|
2023-10-17 15:08:46 +02:00
|
|
|
self.position.pieces.white.pawns.delete(self.position.pieces.white.pawns.find(location))
|
2023-10-15 16:53:44 +02:00
|
|
|
of Bishop:
|
2023-10-17 15:08:46 +02:00
|
|
|
self.position.pieces.white.pawns.delete(self.position.pieces.white.bishops.find(location))
|
2023-10-15 16:53:44 +02:00
|
|
|
of Knight:
|
2023-10-17 15:08:46 +02:00
|
|
|
self.position.pieces.white.pawns.delete(self.position.pieces.white.knights.find(location))
|
2023-10-15 16:53:44 +02:00
|
|
|
of Rook:
|
2023-10-17 15:08:46 +02:00
|
|
|
self.position.pieces.white.rooks.delete(self.position.pieces.white.rooks.find(location))
|
2023-10-15 16:53:44 +02:00
|
|
|
of Queen:
|
2023-10-17 15:08:46 +02:00
|
|
|
self.position.pieces.white.queens.delete(self.position.pieces.white.queens.find(location))
|
2023-10-15 16:53:44 +02:00
|
|
|
of King:
|
|
|
|
doAssert false, "removePiece: attempted to remove the white king"
|
|
|
|
else:
|
|
|
|
discard
|
|
|
|
of Black:
|
|
|
|
case piece.kind:
|
|
|
|
of Pawn:
|
2023-10-20 02:23:07 +02:00
|
|
|
self.position.pieces.black.pawns.delete(self.position.pieces.black.pawns.find(location))
|
2023-10-15 16:53:44 +02:00
|
|
|
of Bishop:
|
2023-10-17 15:08:46 +02:00
|
|
|
self.position.pieces.black.bishops.delete(self.position.pieces.black.bishops.find(location))
|
2023-10-15 16:53:44 +02:00
|
|
|
of Knight:
|
2023-10-17 15:08:46 +02:00
|
|
|
self.position.pieces.black.knights.delete(self.position.pieces.black.knights.find(location))
|
2023-10-15 16:53:44 +02:00
|
|
|
of Rook:
|
2023-10-17 15:08:46 +02:00
|
|
|
self.position.pieces.black.rooks.delete(self.position.pieces.black.rooks.find(location))
|
2023-10-15 16:53:44 +02:00
|
|
|
of Queen:
|
2023-10-17 15:08:46 +02:00
|
|
|
self.position.pieces.black.queens.delete(self.position.pieces.black.queens.find(location))
|
2023-10-15 16:53:44 +02:00
|
|
|
of King:
|
|
|
|
doAssert false, "removePiece: attempted to remove the black king"
|
|
|
|
else:
|
|
|
|
discard
|
|
|
|
else:
|
|
|
|
discard
|
|
|
|
|
|
|
|
|
2023-10-20 02:23:07 +02:00
|
|
|
proc movePiece(self: ChessBoard, move: Move, attack: bool = true) =
|
|
|
|
## Internal helper to move a piece. If attack
|
|
|
|
## is set to false, then this function does
|
|
|
|
## not update attacked squares metadata, just
|
|
|
|
## positional info and the grid itself
|
2023-10-15 16:53:44 +02:00
|
|
|
case move.piece.color:
|
|
|
|
of White:
|
|
|
|
case move.piece.kind:
|
|
|
|
of Pawn:
|
|
|
|
# The way things are structured, we don't care about the order
|
|
|
|
# of this list, so we can add and remove entries as we please
|
2023-10-17 15:08:46 +02:00
|
|
|
self.position.pieces.white.pawns.delete(self.position.pieces.white.pawns.find(move.startSquare))
|
|
|
|
self.position.pieces.white.pawns.add(move.targetSquare)
|
2023-10-15 16:53:44 +02:00
|
|
|
of Bishop:
|
2023-10-17 15:08:46 +02:00
|
|
|
self.position.pieces.white.bishops.delete(self.position.pieces.white.bishops.find(move.startSquare))
|
|
|
|
self.position.pieces.white.bishops.add(move.targetSquare)
|
2023-10-15 16:53:44 +02:00
|
|
|
of Knight:
|
2023-10-17 15:08:46 +02:00
|
|
|
self.position.pieces.white.knights.delete(self.position.pieces.white.knights.find(move.startSquare))
|
|
|
|
self.position.pieces.white.knights.add(move.targetSquare)
|
2023-10-15 16:53:44 +02:00
|
|
|
of Rook:
|
2023-10-17 15:08:46 +02:00
|
|
|
self.position.pieces.white.rooks.delete(self.position.pieces.white.rooks.find(move.startSquare))
|
|
|
|
self.position.pieces.white.rooks.add(move.targetSquare)
|
2023-10-15 16:53:44 +02:00
|
|
|
of Queen:
|
2023-10-17 22:16:01 +02:00
|
|
|
self.position.pieces.white.queens.delete(self.position.pieces.white.queens.find(move.startSquare))
|
2023-10-17 15:08:46 +02:00
|
|
|
self.position.pieces.white.queens.add(move.targetSquare)
|
2023-10-15 16:53:44 +02:00
|
|
|
of King:
|
2023-10-17 15:08:46 +02:00
|
|
|
self.position.pieces.white.king = move.targetSquare
|
2023-10-15 16:53:44 +02:00
|
|
|
else:
|
2023-10-16 14:55:43 +02:00
|
|
|
discard
|
2023-10-15 16:53:44 +02:00
|
|
|
of Black:
|
|
|
|
case move.piece.kind:
|
|
|
|
of Pawn:
|
2023-10-17 15:08:46 +02:00
|
|
|
self.position.pieces.black.pawns.delete(self.position.pieces.black.pawns.find(move.startSquare))
|
|
|
|
self.position.pieces.black.pawns.add(move.targetSquare)
|
2023-10-15 16:53:44 +02:00
|
|
|
of Bishop:
|
2023-10-17 15:08:46 +02:00
|
|
|
self.position.pieces.black.bishops.delete(self.position.pieces.black.bishops.find(move.startSquare))
|
|
|
|
self.position.pieces.black.bishops.add(move.targetSquare)
|
2023-10-15 16:53:44 +02:00
|
|
|
of Knight:
|
2023-10-17 15:08:46 +02:00
|
|
|
self.position.pieces.black.knights.delete(self.position.pieces.black.knights.find(move.startSquare))
|
|
|
|
self.position.pieces.black.knights.add(move.targetSquare)
|
2023-10-15 16:53:44 +02:00
|
|
|
of Rook:
|
2023-10-17 15:08:46 +02:00
|
|
|
self.position.pieces.black.rooks.delete(self.position.pieces.black.rooks.find(move.startSquare))
|
|
|
|
self.position.pieces.black.rooks.add(move.targetSquare)
|
2023-10-15 16:53:44 +02:00
|
|
|
of Queen:
|
2023-10-17 15:08:46 +02:00
|
|
|
self.position.pieces.black.queens.delete(self.position.pieces.black.queens.find(move.startSquare))
|
|
|
|
self.position.pieces.black.queens.add(move.targetSquare)
|
2023-10-15 16:53:44 +02:00
|
|
|
of King:
|
2023-10-17 15:08:46 +02:00
|
|
|
self.position.pieces.black.king = move.targetSquare
|
2023-10-15 16:53:44 +02:00
|
|
|
else:
|
|
|
|
discard
|
|
|
|
else:
|
2023-10-17 15:08:46 +02:00
|
|
|
discard
|
2023-10-21 18:19:41 +02:00
|
|
|
# Empty out the starting square
|
2023-10-16 14:55:43 +02:00
|
|
|
self.grid[move.startSquare.row, move.startSquare.col] = emptyPiece()
|
2023-10-15 16:53:44 +02:00
|
|
|
# Actually move the piece
|
2023-10-20 02:23:07 +02:00
|
|
|
self.grid[move.targetSquare.row, move.targetSquare.col] = move.piece
|
|
|
|
if attack:
|
|
|
|
self.updateAttackedSquares()
|
|
|
|
|
|
|
|
|
|
|
|
proc movePiece(self: ChessBoard, startSquare, targetSquare: Location, attack: bool = true) =
|
|
|
|
## Like the other movePiece(), but with two locations
|
|
|
|
self.movePiece(Move(startSquare: startSquare, targetSquare: targetSquare,
|
|
|
|
piece: self.grid[startSquare.row, startSquare.col],
|
|
|
|
),
|
|
|
|
attack
|
|
|
|
)
|
2023-10-17 15:08:46 +02:00
|
|
|
|
|
|
|
|
2023-10-20 02:23:07 +02:00
|
|
|
proc updateLocations(self: ChessBoard, move: Move) =
|
2023-10-17 15:08:46 +02:00
|
|
|
## Internal helper to update the position of
|
|
|
|
## the pieces on the board after a move
|
|
|
|
let capture = self.getCapture(move)
|
|
|
|
if capture != emptyLocation():
|
2023-10-17 16:38:43 +02:00
|
|
|
self.position.captured = self.grid[capture.row, capture.col]
|
2023-10-20 02:23:07 +02:00
|
|
|
self.removePiece(capture)
|
2023-10-17 15:08:46 +02:00
|
|
|
# Update the positional metadata of the moving piece
|
|
|
|
self.movePiece(move)
|
2023-10-21 18:19:41 +02:00
|
|
|
|
2023-10-15 16:53:44 +02:00
|
|
|
|
|
|
|
proc doMove(self: ChessBoard, move: Move) =
|
|
|
|
## Internal function called by makeMove after
|
|
|
|
## performing legality checks on the given move. Can
|
|
|
|
## be used in performance-critical paths where
|
|
|
|
## a move is already known to be legal
|
2023-10-17 15:08:46 +02:00
|
|
|
|
|
|
|
# Final checks
|
|
|
|
|
2023-10-20 02:23:07 +02:00
|
|
|
# Record the final move in the position
|
2023-10-17 22:16:01 +02:00
|
|
|
self.position.move = move
|
|
|
|
|
2023-10-17 15:08:46 +02:00
|
|
|
# Needed to detect draw by the 50 move rule
|
2023-10-17 17:27:33 +02:00
|
|
|
var
|
|
|
|
halfMoveClock = self.position.halfMoveClock
|
|
|
|
fullMoveCount = self.position.fullMoveCount
|
2023-10-17 22:16:01 +02:00
|
|
|
castlingAvailable = self.position.castlingAvailable
|
2023-10-21 18:19:41 +02:00
|
|
|
let capture = self.getCapture(move)
|
|
|
|
if move.piece.kind == Pawn or self.isCapture(move):
|
|
|
|
halfMoveClock = 0
|
|
|
|
else:
|
|
|
|
inc(halfMoveClock)
|
|
|
|
if move.piece.color == Black:
|
|
|
|
inc(fullMoveCount)
|
|
|
|
# Castling check: have the rooks moved?
|
|
|
|
if move.piece.kind == Rook:
|
|
|
|
case move.piece.color:
|
|
|
|
of White:
|
|
|
|
if move.startSquare.row == move.piece.getStartRow():
|
|
|
|
if move.startSquare.col == 0:
|
|
|
|
# Queen side
|
|
|
|
castlingAvailable.white.queen = false
|
|
|
|
elif move.startSquare.col == 7:
|
|
|
|
# King side
|
|
|
|
castlingAvailable.white.king = false
|
|
|
|
of Black:
|
|
|
|
if move.startSquare.row == move.piece.getStartRow():
|
|
|
|
if move.startSquare.col == 0:
|
|
|
|
# Queen side
|
|
|
|
castlingAvailable.black.queen = false
|
|
|
|
elif move.startSquare.col == 7:
|
|
|
|
# King side
|
|
|
|
castlingAvailable.black.king = false
|
|
|
|
else:
|
|
|
|
discard
|
|
|
|
# Has a rook been captured?
|
|
|
|
if capture != emptyLocation():
|
|
|
|
let piece = self.grid[capture.row, capture.col]
|
|
|
|
if piece.kind == Rook:
|
|
|
|
case piece.color:
|
2023-10-17 23:56:26 +02:00
|
|
|
of White:
|
2023-10-21 18:19:41 +02:00
|
|
|
if capture == piece.color.queenSideRook():
|
|
|
|
# Queen side
|
|
|
|
castlingAvailable.white.queen = false
|
|
|
|
elif capture == piece.color.kingSideRook():
|
|
|
|
# King side
|
|
|
|
castlingAvailable.white.king = false
|
2023-10-17 23:56:26 +02:00
|
|
|
of Black:
|
2023-10-21 18:19:41 +02:00
|
|
|
if capture == piece.color.queenSideRook():
|
|
|
|
# Queen side
|
|
|
|
castlingAvailable.black.queen = false
|
|
|
|
elif capture == piece.color.kingSideRook():
|
|
|
|
# King side
|
|
|
|
castlingAvailable.black.king = false
|
2023-10-17 23:56:26 +02:00
|
|
|
else:
|
2023-10-21 18:19:41 +02:00
|
|
|
# Unreachable
|
2023-10-17 23:56:26 +02:00
|
|
|
discard
|
2023-10-21 18:19:41 +02:00
|
|
|
# Has the king moved?
|
|
|
|
if move.piece.kind == King:
|
|
|
|
case move.piece.color:
|
|
|
|
of White:
|
|
|
|
castlingAvailable.white.king = false
|
|
|
|
castlingAvailable.white.queen = false
|
|
|
|
of Black:
|
|
|
|
castlingAvailable.black.king = false
|
|
|
|
castlingAvailable.black.queen = false
|
2023-10-17 22:16:01 +02:00
|
|
|
else:
|
2023-10-21 18:19:41 +02:00
|
|
|
discard
|
2023-10-20 02:23:07 +02:00
|
|
|
|
2023-10-21 18:19:41 +02:00
|
|
|
let previous = self.position
|
|
|
|
# Record final position for future reference
|
|
|
|
self.positions.add(previous)
|
|
|
|
# Create new position
|
|
|
|
self.position = Position(plyFromRoot: self.position.plyFromRoot + 1,
|
|
|
|
halfMoveClock: halfMoveClock,
|
|
|
|
fullMoveCount: fullMoveCount,
|
|
|
|
captured: emptyPiece(),
|
|
|
|
turn: self.getActiveColor().opposite,
|
|
|
|
castlingAvailable: castlingAvailable,
|
|
|
|
# Updated at the next call to doMove()
|
|
|
|
move: emptyMove(),
|
|
|
|
pieces: previous.pieces,
|
|
|
|
)
|
|
|
|
|
|
|
|
var location: Location
|
|
|
|
if move.flag in [CastleShort, CastleLong]:
|
|
|
|
# Move the rook onto the
|
|
|
|
# correct file
|
|
|
|
var
|
|
|
|
location: Location
|
|
|
|
target: Location
|
|
|
|
if move.flag == CastleShort:
|
|
|
|
location = move.piece.color.kingSideRook()
|
|
|
|
target = shortCastleRook()
|
|
|
|
else:
|
|
|
|
location = move.piece.color.queenSideRook()
|
|
|
|
target = longCastleRook()
|
|
|
|
let rook = self.grid[location.row, location.col]
|
|
|
|
let move = Move(startSquare: location, targetSquare: location + target, piece: rook, flag: move.flag)
|
|
|
|
self.movePiece(move, attack=false)
|
2023-10-17 22:16:01 +02:00
|
|
|
|
2023-10-21 18:19:41 +02:00
|
|
|
# Update position and attack metadata
|
|
|
|
self.updateLocations(move)
|
|
|
|
|
2023-10-17 16:38:43 +02:00
|
|
|
# Check for double pawn push
|
2023-10-20 02:23:07 +02:00
|
|
|
if move.flag == DoublePush:
|
|
|
|
self.position.enPassantSquare = Move(piece: move.piece,
|
2023-10-17 16:38:43 +02:00
|
|
|
startSquare: (move.startSquare.row, move.startSquare.col),
|
2023-10-17 22:16:01 +02:00
|
|
|
targetSquare: move.targetSquare + move.piece.color.bottomSide())
|
2023-10-17 16:38:43 +02:00
|
|
|
else:
|
2023-10-20 02:23:07 +02:00
|
|
|
self.position.enPassantSquare = emptyMove()
|
2023-10-17 15:08:46 +02:00
|
|
|
|
|
|
|
|
|
|
|
proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) =
|
|
|
|
## Internal helper to "spawn" a given piece at the given
|
|
|
|
## location. Note that this will overwrite whatever piece
|
|
|
|
## was previously located there: use with caution. Does
|
|
|
|
## not automatically update the attacked square metadata
|
|
|
|
## or other positional information
|
|
|
|
case piece.color:
|
|
|
|
of White:
|
|
|
|
case piece.kind:
|
|
|
|
of Pawn:
|
|
|
|
self.position.pieces.white.pawns.add(location)
|
|
|
|
of Knight:
|
|
|
|
self.position.pieces.white.knights.add(location)
|
|
|
|
of Bishop:
|
|
|
|
self.position.pieces.white.bishops.add(location)
|
|
|
|
of Rook:
|
|
|
|
self.position.pieces.white.rooks.add(location)
|
|
|
|
of Queen:
|
|
|
|
self.position.pieces.white.queens.add(location)
|
2023-10-17 22:16:01 +02:00
|
|
|
of King:
|
|
|
|
self.position.pieces.white.king = location
|
2023-10-17 15:08:46 +02:00
|
|
|
else:
|
|
|
|
discard
|
|
|
|
of Black:
|
|
|
|
case piece.kind:
|
|
|
|
of Pawn:
|
|
|
|
self.position.pieces.black.pawns.add(location)
|
|
|
|
of Knight:
|
|
|
|
self.position.pieces.black.knights.add(location)
|
|
|
|
of Bishop:
|
|
|
|
self.position.pieces.black.bishops.add(location)
|
|
|
|
of Rook:
|
|
|
|
self.position.pieces.black.rooks.add(location)
|
|
|
|
of Queen:
|
|
|
|
self.position.pieces.black.queens.add(location)
|
2023-10-17 22:16:01 +02:00
|
|
|
of King:
|
|
|
|
self.position.pieces.black.king = location
|
2023-10-17 15:08:46 +02:00
|
|
|
else:
|
|
|
|
discard
|
|
|
|
else:
|
|
|
|
# Unreachable
|
|
|
|
discard
|
|
|
|
self.grid[location.row, location.col] = piece
|
|
|
|
|
|
|
|
|
2023-10-21 18:19:41 +02:00
|
|
|
proc undoMove*(self: ChessBoard, move: Move) =
|
|
|
|
## Undoes the given move
|
2023-10-17 15:08:46 +02:00
|
|
|
if self.positions.len() == 0:
|
|
|
|
return
|
2023-10-21 18:19:41 +02:00
|
|
|
var position = self.positions[^1]
|
|
|
|
while true:
|
|
|
|
if position.move == move:
|
|
|
|
break
|
|
|
|
discard self.positions.pop()
|
|
|
|
position = self.positions[^1]
|
2023-10-17 15:08:46 +02:00
|
|
|
|
2023-10-21 18:19:41 +02:00
|
|
|
self.grid[move.startSquare.row, move.startSquare.col] = move.piece
|
|
|
|
if self.isCapture(move):
|
|
|
|
self.grid[move.targetSquare.row, move.targetSquare.col] = self.position.captured
|
|
|
|
else:
|
|
|
|
self.grid[move.targetSquare.row, move.targetSquare.col] = emptyPiece()
|
|
|
|
self.position = position
|
|
|
|
|
|
|
|
|
2023-10-17 10:31:38 +02:00
|
|
|
|
2023-10-20 02:23:07 +02:00
|
|
|
proc isLegal(self: ChessBoard, move: Move, keep: bool = false): bool =
|
2023-10-17 16:38:43 +02:00
|
|
|
## Returns whether the given move is legal
|
2023-10-21 18:19:41 +02:00
|
|
|
self.doMove(move)
|
2023-10-17 12:08:07 +02:00
|
|
|
if move notin self.generateMoves(move.startSquare):
|
2023-10-17 22:16:01 +02:00
|
|
|
# Piece cannot arrive to destination (blocked
|
|
|
|
# or otherwise invalid move)
|
2023-10-17 10:31:38 +02:00
|
|
|
return false
|
2023-10-20 02:23:07 +02:00
|
|
|
if not keep:
|
|
|
|
defer: self.undoMove(move)
|
|
|
|
# Move would reveal an attack
|
|
|
|
# on our king: not allowed
|
2023-10-21 18:19:41 +02:00
|
|
|
return self.inCheck(move.piece.color)
|
2023-10-20 02:23:07 +02:00
|
|
|
|
|
|
|
|
|
|
|
proc isLegalFast(self: ChessBoard, move: Move, keep: bool = false): bool =
|
|
|
|
## Returns whether the given move is legal
|
|
|
|
## assuming that the input move is pseudo legal
|
|
|
|
self.position.move = move
|
|
|
|
self.doMove(move)
|
|
|
|
if not keep:
|
|
|
|
defer: self.undoMove(move)
|
2023-10-21 18:19:41 +02:00
|
|
|
|
2023-10-17 10:31:38 +02:00
|
|
|
# Move would reveal an attack
|
|
|
|
# on our king: not allowed
|
2023-10-17 12:08:07 +02:00
|
|
|
if self.inCheck(move.piece.color):
|
2023-10-17 10:31:38 +02:00
|
|
|
return false
|
|
|
|
# All checks have passed: move is legal
|
|
|
|
result = true
|
|
|
|
|
|
|
|
|
|
|
|
proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} =
|
|
|
|
## Like the other makeMove(), but with a Move object
|
|
|
|
result = move
|
2023-10-20 02:23:07 +02:00
|
|
|
self.position.move = move
|
|
|
|
let legal = self.isLegal(move, keep=true)
|
|
|
|
if not legal:
|
2023-10-16 14:55:43 +02:00
|
|
|
return emptyMove()
|
2023-10-20 02:23:07 +02:00
|
|
|
result = self.position.move
|
|
|
|
|
|
|
|
|
2023-10-15 22:46:22 +02:00
|
|
|
proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.discardable.} =
|
2023-10-15 16:53:44 +02:00
|
|
|
## Makes a move on the board from the chosen start square to
|
|
|
|
## the chosen target square, ensuring it is legal (turns are
|
2023-10-15 22:46:22 +02:00
|
|
|
## taken into account!). This function returns a Move object: if the move
|
2023-10-15 16:53:44 +02:00
|
|
|
## is legal and has been performed, the fields will be populated properly.
|
|
|
|
## For efficiency purposes, no exceptions are raised if the move is
|
|
|
|
## 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)
|
2023-10-17 10:31:38 +02:00
|
|
|
var
|
2023-10-17 16:38:43 +02:00
|
|
|
startLocation = startSquare.algebraicToLocation()
|
|
|
|
targetLocation = targetSquare.algebraicToLocation()
|
2023-10-17 10:31:38 +02:00
|
|
|
result = Move(startSquare: startLocation, targetSquare: targetLocation, piece: self.grid[startLocation.row, startLocation.col])
|
|
|
|
return self.makeMove(result)
|
2023-10-15 16:53:44 +02:00
|
|
|
|
|
|
|
|
2023-10-17 15:08:46 +02:00
|
|
|
proc makeMove*(self: ChessBoard, startSquare, targetSquare: Location): Move {.discardable.} =
|
|
|
|
## Like the other makeMove(), but with two locations
|
|
|
|
result = Move(startSquare: startSquare, targetSquare: targetSquare, piece: self.grid[startSquare.row, startSquare.col])
|
|
|
|
return self.makeMove(result)
|
|
|
|
|
|
|
|
|
2023-10-15 16:53:44 +02:00
|
|
|
proc `$`*(self: ChessBoard): string =
|
|
|
|
result &= "- - - - - - - -"
|
|
|
|
for i, row in self.grid:
|
|
|
|
result &= "\n"
|
|
|
|
for piece in row:
|
|
|
|
if piece.kind == Empty:
|
2023-10-15 22:46:22 +02:00
|
|
|
result &= "x "
|
2023-10-15 16:53:44 +02:00
|
|
|
continue
|
|
|
|
if piece.color == White:
|
|
|
|
result &= &"{char(piece.kind).toUpperAscii()} "
|
|
|
|
else:
|
|
|
|
result &= &"{char(piece.kind)} "
|
|
|
|
result &= &"{rankToColumn(i + 1) + 1}"
|
|
|
|
result &= "\n- - - - - - - -"
|
|
|
|
result &= "\na b c d e f g h"
|
|
|
|
|
|
|
|
|
2023-10-15 22:46:22 +02:00
|
|
|
proc pretty*(self: ChessBoard): string =
|
|
|
|
## Returns a colorized version of the
|
|
|
|
## board for easier visualization
|
|
|
|
result &= "- - - - - - - -"
|
|
|
|
|
|
|
|
for i, row in self.grid:
|
|
|
|
result &= "\n"
|
|
|
|
for j, piece in row:
|
|
|
|
if piece.kind == Empty:
|
|
|
|
result &= "\x1b[36;1mx"
|
|
|
|
# Avoids the color overflowing
|
|
|
|
# onto the numbers
|
|
|
|
if j < 7:
|
|
|
|
result &= " \x1b[0m"
|
|
|
|
else:
|
|
|
|
result &= "\x1b[0m "
|
|
|
|
continue
|
|
|
|
if piece.color == White:
|
|
|
|
result &= &"\x1b[37;1m{char(piece.kind).toUpperAscii()}\x1b[0m "
|
|
|
|
else:
|
|
|
|
result &= &"\x1b[30;1m{char(piece.kind)} "
|
|
|
|
result &= &"\x1b[33;1m{rankToColumn(i + 1) + 1}\x1b[0m"
|
|
|
|
|
|
|
|
result &= "\n- - - - - - - -"
|
|
|
|
result &= "\n\x1b[31;1ma b c d e f g h"
|
|
|
|
result &= "\x1b[0m"
|
|
|
|
|
2023-10-15 16:53:44 +02:00
|
|
|
|
2023-10-21 18:19:41 +02:00
|
|
|
proc countLegalMoves*(self: ChessBoard, ply: int, verbose: bool = false, current: int = 0): int =
|
2023-10-20 02:23:07 +02:00
|
|
|
## Counts (and debugs) the number of legal positions reached after
|
|
|
|
## the given number of half moves
|
|
|
|
if ply == 0:
|
|
|
|
result = 1
|
|
|
|
else:
|
2023-10-21 18:19:41 +02:00
|
|
|
var before: string
|
2023-10-20 02:23:07 +02:00
|
|
|
for move in self.generateAllMoves():
|
2023-10-21 18:19:41 +02:00
|
|
|
before = self.pretty()
|
|
|
|
self.doMove(move)
|
|
|
|
if not self.inCheck(move.piece.color):
|
2023-10-20 02:23:07 +02:00
|
|
|
if verbose:
|
|
|
|
let canCastle = self.canCastle()
|
|
|
|
echo "\x1Bc"
|
|
|
|
echo &"Ply: {self.position.plyFromRoot} (move {current + result + 1})"
|
|
|
|
echo &"Move: {move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()} (({move.startSquare.row}, {move.startSquare.col}) -> ({move.targetSquare.row}, {move.targetSquare.col}))"
|
|
|
|
echo &"Turn: {move.piece.color}"
|
|
|
|
echo &"Piece: {move.piece.kind}"
|
|
|
|
echo &"Flag: {move.flag}"
|
|
|
|
echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
|
|
|
|
echo "\nBefore:"
|
2023-10-21 18:19:41 +02:00
|
|
|
echo before
|
2023-10-20 02:23:07 +02:00
|
|
|
echo "\nNow: "
|
|
|
|
echo self.pretty()
|
|
|
|
try:
|
|
|
|
discard readLine(stdin)
|
|
|
|
except IOError:
|
|
|
|
discard
|
|
|
|
except EOFError:
|
|
|
|
discard
|
2023-10-21 18:19:41 +02:00
|
|
|
result += self.countLegalMoves(ply - 1, verbose, result + 1)
|
2023-10-20 02:23:07 +02:00
|
|
|
self.undoMove(move)
|
|
|
|
|
2023-10-21 18:19:41 +02:00
|
|
|
|
|
|
|
|
2023-10-13 11:54:57 +02:00
|
|
|
when isMainModule:
|
|
|
|
proc testPiece(piece: Piece, kind: PieceKind, color: PieceColor) =
|
|
|
|
doAssert piece.kind == kind and piece.color == color, &"expected piece of kind {kind} and color {color}, got {piece.kind} / {piece.color} instead"
|
|
|
|
|
2023-10-13 12:26:14 +02:00
|
|
|
proc testPieceCount(board: ChessBoard, kind: PieceKind, color: PieceColor, count: int) =
|
|
|
|
let pieces = board.countPieces(kind, color)
|
|
|
|
doAssert pieces == count, &"expected {count} pieces of kind {kind} and color {color}, got {pieces} instead"
|
|
|
|
|
2023-10-13 11:54:57 +02:00
|
|
|
echo "Running tests"
|
|
|
|
var b = newDefaultChessboard()
|
2023-10-13 12:26:14 +02:00
|
|
|
# Ensure correct number of pieces
|
|
|
|
testPieceCount(b, Pawn, White, 8)
|
|
|
|
testPieceCount(b, Pawn, Black, 8)
|
|
|
|
testPieceCount(b, Knight, White, 2)
|
|
|
|
testPieceCount(b, Knight, Black, 2)
|
|
|
|
testPieceCount(b, Bishop, White, 2)
|
|
|
|
testPieceCount(b, Bishop, Black, 2)
|
|
|
|
testPieceCount(b, Rook, White, 2)
|
|
|
|
testPieceCount(b, Rook, Black, 2)
|
|
|
|
testPieceCount(b, Queen, White, 1)
|
|
|
|
testPieceCount(b, Queen, Black, 1)
|
|
|
|
testPieceCount(b, King, White, 1)
|
|
|
|
testPieceCount(b, King, Black, 1)
|
|
|
|
|
|
|
|
|
|
|
|
# Ensure pieces are in the correct location
|
|
|
|
|
|
|
|
# Pawns
|
2023-10-13 11:54:57 +02:00
|
|
|
for loc in ["a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2"]:
|
|
|
|
testPiece(b.getPiece(loc), Pawn, White)
|
|
|
|
for loc in ["a7", "b7", "c7", "d7", "e7", "f7", "g7", "h7"]:
|
|
|
|
testPiece(b.getPiece(loc), Pawn, Black)
|
|
|
|
# Rooks
|
|
|
|
testPiece(b.getPiece("a1"), Rook, White)
|
|
|
|
testPiece(b.getPiece("h1"), Rook, White)
|
|
|
|
testPiece(b.getPiece("a8"), Rook, Black)
|
|
|
|
testPiece(b.getPiece("h8"), Rook, Black)
|
|
|
|
# Knights
|
|
|
|
testPiece(b.getPiece("b1"), Knight, White)
|
|
|
|
testPiece(b.getPiece("g1"), Knight, White)
|
|
|
|
testPiece(b.getPiece("b8"), Knight, Black)
|
|
|
|
testPiece(b.getPiece("g8"), Knight, Black)
|
|
|
|
# Bishops
|
|
|
|
testPiece(b.getPiece("c1"), Bishop, White)
|
|
|
|
testPiece(b.getPiece("f1"), Bishop, White)
|
|
|
|
testPiece(b.getPiece("c8"), Bishop, Black)
|
|
|
|
testPiece(b.getPiece("f8"), Bishop, Black)
|
|
|
|
# Kings
|
|
|
|
testPiece(b.getPiece("e1"), King, White)
|
|
|
|
testPiece(b.getPiece("e8"), King, Black)
|
|
|
|
# Queens
|
|
|
|
testPiece(b.getPiece("d1"), Queen, White)
|
|
|
|
testPiece(b.getPiece("d8"), Queen, Black)
|
2023-10-18 10:45:54 +02:00
|
|
|
|
|
|
|
when compileOption("profiler"):
|
|
|
|
import nimprof
|
|
|
|
|
2023-10-21 18:19:41 +02:00
|
|
|
|
|
|
|
echo b.countLegalMoves(4, verbose=false)
|
2023-10-13 11:54:57 +02:00
|
|
|
echo "All tests were successful"
|