2289 lines
92 KiB
Nim
2289 lines
92 KiB
Nim
# 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 std/strutils
|
|
import std/strformat
|
|
import std/times
|
|
import std/math
|
|
|
|
|
|
type
|
|
|
|
PieceColor* = enum
|
|
## A piece color enumeration
|
|
None = 0'i8,
|
|
White,
|
|
Black
|
|
|
|
PieceKind* = enum
|
|
## A chess piece enumeration
|
|
Empty = 0'i8, # No piece
|
|
Bishop = 'b',
|
|
King = 'k'
|
|
Knight = 'n',
|
|
Pawn = 'p',
|
|
Queen = 'q',
|
|
Rook = 'r',
|
|
|
|
Piece* = object
|
|
## A chess piece
|
|
color*: PieceColor
|
|
kind*: PieceKind
|
|
|
|
MoveFlag* = enum
|
|
## An enumeration of move flags
|
|
Default = 0'u16, # No flag
|
|
EnPassant = 1, # Move is a capture with en passant
|
|
Capture = 2, # Move is a capture
|
|
DoublePush = 4, # Move is a double pawn push
|
|
# Castling metadata
|
|
CastleLong = 8,
|
|
CastleShort = 16,
|
|
# Pawn promotion metadata
|
|
PromoteToQueen = 32,
|
|
PromoteToRook = 64,
|
|
PromoteToBishop = 128,
|
|
PromoteToKnight = 256
|
|
|
|
# Useful type aliases
|
|
Location* = tuple[row, col: int8]
|
|
|
|
Attacked = seq[tuple[source, target, direction: Location]]
|
|
|
|
Pieces = tuple[king: Location, queens: seq[Location], rooks: seq[Location],
|
|
bishops: seq[Location], knights: seq[Location],
|
|
pawns: seq[Location]]
|
|
|
|
CountData = tuple[nodes: uint64, captures: uint64, castles: uint64, checks: uint64, promotions: uint64, enPassant: uint64, checkmates: uint64]
|
|
|
|
Move* = object
|
|
## A chess move
|
|
startSquare*: Location
|
|
targetSquare*: Location
|
|
flags*: uint16
|
|
|
|
|
|
Position* = ref object
|
|
## A chess position
|
|
# Did the rooks on either side or the king move?
|
|
castlingAvailable: tuple[white, black: tuple[queen, king: bool]]
|
|
# Number of half-moves that were performed
|
|
# to reach this position starting from the
|
|
# root of the tree
|
|
plyFromRoot: int16
|
|
# Number of half moves since
|
|
# last piece capture or pawn movement.
|
|
# Used for the 50-move rule
|
|
halfMoveClock: int8
|
|
# Full move counter. Increments
|
|
# every 2 ply
|
|
fullMoveCount: int16
|
|
# En passant target square (see https://en.wikipedia.org/wiki/En_passant)
|
|
enPassantSquare*: Location
|
|
# Locations of all pieces
|
|
pieces: tuple[white: Pieces, black: Pieces]
|
|
# Squares attacked by both sides
|
|
attacked: tuple[white: Attacked, black: Attacked]
|
|
# Pieces pinned by both sides (only absolute pins)
|
|
pinned: tuple[white: Attacked, black: Attacked]
|
|
# Active color
|
|
turn: PieceColor
|
|
|
|
ChessBoard* = ref object
|
|
## A chess board object
|
|
|
|
# The actual board where pieces live
|
|
# (flattened 8x8 matrix)
|
|
grid: array[64, Piece]
|
|
# The current position
|
|
position: Position
|
|
# List of all previously reached positions
|
|
positions: seq[Position]
|
|
|
|
|
|
# A bunch of simple utility functions and forward declarations
|
|
|
|
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 algebraicToLocation*(s: string): Location {.inline.}
|
|
proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.}
|
|
func emptyMove*: Move {.inline.} = Move(startSquare: emptyLocation(), targetSquare: emptyLocation())
|
|
func `+`*(a, b: Location): Location = (a.row + b.row, a.col + b.col)
|
|
func `-`*(a: Location): Location = (-a.row, -a.col)
|
|
func `-`*(a, b: Location): Location = (a.row - b.row, a.col - b.col)
|
|
func isValid*(a: Location): bool {.inline.} = a.row in 0..7 and a.col in 0..7
|
|
proc generateMoves(self: ChessBoard, location: Location): seq[Move]
|
|
proc getAttackers*(self: ChessBoard, loc: Location, color: PieceColor): seq[Location]
|
|
proc getAttackFor*(self: ChessBoard, source, target: Location): tuple[source, target, direction: Location]
|
|
proc isAttacked*(self: ChessBoard, loc: Location, color: PieceColor = None): bool
|
|
proc isLegal(self: ChessBoard, move: Move): bool {.inline.}
|
|
proc doMove(self: ChessBoard, move: Move)
|
|
proc pretty*(self: ChessBoard): string
|
|
proc spawnPiece(self: ChessBoard, location: Location, piece: Piece)
|
|
proc updateAttackedSquares(self: ChessBoard)
|
|
proc updateSlidingAttacks(self: ChessBoard)
|
|
proc getPinnedDirections(self: ChessBoard, loc: Location): seq[Location]
|
|
proc getAttacks*(self: ChessBoard, loc: Location): Attacked
|
|
proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked, pins: Attacked]
|
|
proc inCheck*(self: ChessBoard, color: PieceColor = None): bool
|
|
proc toFEN*(self: ChessBoard): string
|
|
proc undoLastMove*(self: ChessBoard)
|
|
proc movePiece(self: ChessBoard, move: Move, attack: bool = true)
|
|
proc removePiece(self: ChessBoard, location: Location, attack: bool = true)
|
|
|
|
|
|
proc extend[T](self: var seq[T], other: openarray[T]) {.inline.} =
|
|
for x in other:
|
|
self.add(x)
|
|
|
|
proc updateBoard*(self: ChessBoard)
|
|
|
|
# Due to our board layout, directions of movement are reversed for white and black, so
|
|
# we need these helpers to avoid going mad with integer tuples and minus signs everywhere
|
|
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))
|
|
func topSide(color: PieceColor): Location {.inline.} = (if color == White: (-1, 0) else: (1, 0))
|
|
func bottomSide(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))
|
|
func longCastleKing: Location {.inline.} = (0, -2)
|
|
func shortCastleKing: Location {.inline.} = (0, 2)
|
|
func longCastleRook: Location {.inline.} = (0, 3)
|
|
func shortCastleRook: Location {.inline.} = (0, -2)
|
|
func bottomLeftKnightMove(color: PieceColor, long: bool = true): Location {.inline.} =
|
|
if color == White:
|
|
if long:
|
|
return (2, -1)
|
|
else:
|
|
return (1, -2)
|
|
elif color == Black:
|
|
if long:
|
|
return (-2, 1)
|
|
else:
|
|
return (1, -2)
|
|
|
|
|
|
func bottomRightKnightMove(color: PieceColor, long: bool = true): Location {.inline.} =
|
|
if color == White:
|
|
if long:
|
|
return (2, 1)
|
|
else:
|
|
return (1, 2)
|
|
elif color == Black:
|
|
if long:
|
|
return (-2, -1)
|
|
else:
|
|
return (-1, -2)
|
|
|
|
|
|
func topLeftKnightMove(color: PieceColor, long: bool = true): Location {.inline.} =
|
|
if color == White:
|
|
if long:
|
|
return (-2, -1)
|
|
else:
|
|
return (-1, -2)
|
|
elif color == Black:
|
|
if long:
|
|
return (2, 1)
|
|
else:
|
|
return (1, 2)
|
|
|
|
|
|
func topRightKnightMove(color: PieceColor, long: bool = true): Location {.inline.} =
|
|
if color == White:
|
|
if long:
|
|
return (-2, 1)
|
|
else:
|
|
return (-1, 2)
|
|
elif color == Black:
|
|
if long:
|
|
return (2, -1)
|
|
else:
|
|
return (-1, 2)
|
|
|
|
# These return absolute locations rather than relative direction offsets
|
|
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))
|
|
|
|
|
|
# A bunch of getters
|
|
func getActiveColor*(self: ChessBoard): PieceColor {.inline.} =
|
|
## Returns the currently active color
|
|
## (turn of who has to move)
|
|
return self.position.turn
|
|
|
|
|
|
func getEnPassantTarget*(self: ChessBoard): Location {.inline.} =
|
|
## Returns the current en passant target square
|
|
return self.position.enPassantSquare
|
|
|
|
|
|
func getMoveCount*(self: ChessBoard): int {.inline.} =
|
|
## Returns the number of full moves that
|
|
## have been played
|
|
return self.position.fullMoveCount
|
|
|
|
|
|
func getHalfMoveCount*(self: ChessBoard): int {.inline.} =
|
|
## Returns the current number of half-moves
|
|
## since the last irreversible move
|
|
return self.position.halfMoveClock
|
|
|
|
|
|
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:
|
|
return 7
|
|
of Black:
|
|
case piece.kind:
|
|
of Pawn:
|
|
return 1
|
|
else:
|
|
return 0
|
|
|
|
|
|
func getKingStartingPosition(color: PieceColor): Location {.inline.} =
|
|
## Retrieves the starting location of the king
|
|
## for the given color
|
|
case color:
|
|
of White:
|
|
return (7, 4)
|
|
of Black:
|
|
return (0, 4)
|
|
else:
|
|
discard
|
|
|
|
|
|
func getLastRow(color: PieceColor): int {.inline.} =
|
|
## Retrieves the location of the last
|
|
## row relative to the given color
|
|
case color:
|
|
of White:
|
|
return 0
|
|
of Black:
|
|
return 7
|
|
else:
|
|
return -1
|
|
|
|
|
|
proc newChessboard: ChessBoard =
|
|
## Returns a new, empty chessboard
|
|
new(result)
|
|
for i in 0..63:
|
|
result.grid[i] = emptyPiece()
|
|
result.position = Position(attacked: (@[], @[]),
|
|
enPassantSquare: emptyLocation(),
|
|
turn: White,
|
|
fullMoveCount: 1,
|
|
pieces: (white: (king: emptyLocation(),
|
|
queens: @[],
|
|
rooks: @[],
|
|
bishops: @[],
|
|
knights: @[],
|
|
pawns: @[]),
|
|
black: (king: emptyLocation(),
|
|
queens: @[],
|
|
rooks: @[],
|
|
bishops: @[],
|
|
knights: @[],
|
|
pawns: @[])))
|
|
|
|
|
|
func coordToIndex(row, col: int): int {.inline.} = (row * 8) + col
|
|
func `[]`(self: array[64, Piece], row, column: Natural): Piece {.inline.} = self[coordToIndex(row, column)]
|
|
proc `[]=`(self: var array[64, Piece], row, column: Natural, piece: Piece) {.inline.} = self[coordToIndex(row, column)] = piece
|
|
func `[]`(self: array[64, Piece], loc: Location): Piece {.inline.} = self[loc.row, loc.col]
|
|
proc `[]=`(self: var array[64, Piece], loc: Location, piece: Piece) {.inline.} = self[loc.row, loc.col] = piece
|
|
|
|
|
|
proc newChessboardFromFEN*(fen: string): ChessBoard =
|
|
## Initializes a chessboard with the
|
|
## position encoded by the given FEN string
|
|
result = newChessboard()
|
|
var
|
|
# Current location in the grid
|
|
row: int8 = 0
|
|
column: int8 = 0
|
|
# Current section in the FEN string
|
|
section = 0
|
|
# Current index into the FEN string
|
|
index = 0
|
|
# Temporary variable to store a piece
|
|
piece: Piece
|
|
# See https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation
|
|
while index <= fen.high():
|
|
var c = fen[index]
|
|
if c == ' ':
|
|
# Next section
|
|
inc(section)
|
|
inc(index)
|
|
continue
|
|
case section:
|
|
of 0:
|
|
# Piece placement data
|
|
case c.toLowerAscii():
|
|
# Piece
|
|
of 'r', 'n', 'b', 'q', 'k', 'p':
|
|
# We know for a fact these values are in our
|
|
# enumeration, so all is good
|
|
{.warning[HoleEnumConv]:off.}
|
|
piece = Piece(kind: PieceKind(c.toLowerAscii()), color: if c.isUpperAscii(): White else: Black)
|
|
case piece.color:
|
|
of Black:
|
|
case piece.kind:
|
|
of Pawn:
|
|
result.position.pieces.black.pawns.add((row, column))
|
|
of Bishop:
|
|
result.position.pieces.black.bishops.add((row, column))
|
|
of Knight:
|
|
result.position.pieces.black.knights.add((row, column))
|
|
of Rook:
|
|
result.position.pieces.black.rooks.add((row, column))
|
|
of Queen:
|
|
result.position.pieces.black.queens.add((row, column))
|
|
of King:
|
|
if result.position.pieces.black.king != emptyLocation():
|
|
raise newException(ValueError, "invalid position: exactly one king of each color must be present")
|
|
result.position.pieces.black.king = (row, column)
|
|
else:
|
|
discard
|
|
of White:
|
|
case piece.kind:
|
|
of Pawn:
|
|
result.position.pieces.white.pawns.add((row, column))
|
|
of Bishop:
|
|
result.position.pieces.white.bishops.add((row, column))
|
|
of Knight:
|
|
result.position.pieces.white.knights.add((row, column))
|
|
of Rook:
|
|
result.position.pieces.white.rooks.add((row, column))
|
|
of Queen:
|
|
result.position.pieces.white.queens.add((row, column))
|
|
of King:
|
|
if result.position.pieces.white.king != emptyLocation():
|
|
raise newException(ValueError, "invalid position: exactly one king of each color must be present")
|
|
result.position.pieces.white.king = (row, column)
|
|
else:
|
|
discard
|
|
else:
|
|
discard
|
|
result.grid[row, column] = piece
|
|
inc(column)
|
|
of '/':
|
|
# Next row
|
|
inc(row)
|
|
column = 0
|
|
of '0'..'9':
|
|
# Skip x columns
|
|
let x = int(uint8(c) - uint8('0'))
|
|
if x > 8:
|
|
raise newException(ValueError, &"invalid FEN: invalid column skip size ({x} > 8)")
|
|
column += int8(x)
|
|
else:
|
|
raise newException(ValueError, &"invalid FEN: unknown piece identifier '{c}'")
|
|
of 1:
|
|
# Active color
|
|
case c:
|
|
of 'w':
|
|
result.position.turn = White
|
|
of 'b':
|
|
result.position.turn = Black
|
|
else:
|
|
raise newException(ValueError, &"invalid FEN: invalid active color identifier '{c}'")
|
|
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':
|
|
result.position.castlingAvailable.white.king = true
|
|
of 'Q':
|
|
result.position.castlingAvailable.white.queen = true
|
|
of 'k':
|
|
result.position.castlingAvailable.black.king = true
|
|
of 'q':
|
|
result.position.castlingAvailable.black.queen = true
|
|
else:
|
|
raise newException(ValueError, &"invalid FEN: unknown symbol '{c}' found in castling availability section")
|
|
of 3:
|
|
# En passant target square
|
|
case c:
|
|
of '-':
|
|
# Field is already uninitialized to the correct state
|
|
discard
|
|
else:
|
|
result.position.enPassantSquare = fen[index..index+1].algebraicToLocation()
|
|
# Square metadata is 2 bytes long
|
|
inc(index)
|
|
of 4:
|
|
# Halfmove clock
|
|
var s = ""
|
|
while not fen[index].isSpaceAscii():
|
|
s.add(fen[index])
|
|
inc(index)
|
|
# Backtrack so the space is seen by the
|
|
# next iteration of the loop
|
|
dec(index)
|
|
result.position.halfMoveClock = parseInt(s).int8
|
|
of 5:
|
|
# Fullmove number
|
|
var s = ""
|
|
while index <= fen.high():
|
|
s.add(fen[index])
|
|
inc(index)
|
|
result.position.fullMoveCount = parseInt(s).int8
|
|
else:
|
|
raise newException(ValueError, "invalid FEN: too many fields in FEN string")
|
|
inc(index)
|
|
result.updateAttackedSquares()
|
|
if result.inCheck(result.getActiveColor().opposite):
|
|
# Opponent king cannot be captured on the next move
|
|
raise newException(ValueError, "invalid position: opponent king can be captured")
|
|
if result.position.pieces.white.king == emptyLocation() or result.position.pieces.black.king == emptyLocation():
|
|
# Both kings must be on the board
|
|
raise newException(ValueError, "invalid position: exactly one king of each color must be present")
|
|
|
|
|
|
proc newDefaultChessboard*: ChessBoard {.inline.} =
|
|
## Initializes a chessboard with the
|
|
## starting position
|
|
return newChessboardFromFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
|
|
|
|
|
|
proc countPieces*(self: ChessBoard, kind: PieceKind, color: PieceColor): int =
|
|
## Returns the number of pieces with
|
|
## the given color and type in the
|
|
## current position
|
|
case color:
|
|
of White:
|
|
case kind:
|
|
of Pawn:
|
|
return self.position.pieces.white.pawns.len()
|
|
of Bishop:
|
|
return self.position.pieces.white.bishops.len()
|
|
of Knight:
|
|
return self.position.pieces.white.knights.len()
|
|
of Rook:
|
|
return self.position.pieces.white.rooks.len()
|
|
of Queen:
|
|
return self.position.pieces.white.queens.len()
|
|
of King:
|
|
# There shall be only one, forever
|
|
return 1
|
|
else:
|
|
raise newException(ValueError, "invalid piece type")
|
|
of Black:
|
|
case kind:
|
|
of Pawn:
|
|
return self.position.pieces.black.pawns.len()
|
|
of Bishop:
|
|
return self.position.pieces.black.bishops.len()
|
|
of Knight:
|
|
return self.position.pieces.black.knights.len()
|
|
of Rook:
|
|
return self.position.pieces.black.rooks.len()
|
|
of Queen:
|
|
return self.position.pieces.black.queens.len()
|
|
of King:
|
|
# In perpetuity
|
|
return 1
|
|
else:
|
|
raise newException(ValueError, "invalid piece type")
|
|
of None:
|
|
raise newException(ValueError, "invalid piece color")
|
|
|
|
|
|
func countPieces*(self: ChessBoard, piece: Piece): int {.inline.} =
|
|
## Returns the number of pieces on the board that
|
|
## are of the same type and color as the given piece
|
|
return self.countPieces(piece.kind, piece.color)
|
|
|
|
|
|
func rankToColumn(rank: int): int8 {.inline.} =
|
|
## 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
|
|
const indeces: array[8, int8] = [7, 6, 5, 4, 3, 2, 1, 0]
|
|
return indeces[rank - 1]
|
|
|
|
|
|
func rowToFile(row: int): int8 {.inline.} =
|
|
## Converts a row into our grid into
|
|
## a chess file
|
|
const indeces: array[8, int8] = [8, 7, 6, 5, 4, 3, 2, 1]
|
|
return indeces[row]
|
|
|
|
|
|
proc algebraicToLocation*(s: string): Location =
|
|
## 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]}')")
|
|
|
|
let rank = int8(uint8(s[0]) - uint8('a'))
|
|
# Convert the file character to a number
|
|
let file = rankToColumn(int8(uint8(s[1]) - uint8('0')))
|
|
return (file, rank)
|
|
|
|
|
|
func locationToAlgebraic*(loc: Location): string {.inline.} =
|
|
## Converts a location from our internal row, column
|
|
## notation to a square in algebraic notation
|
|
return &"{char(uint8(loc.col) + uint8('a'))}{rowToFile(loc.row)}"
|
|
|
|
|
|
func getPiece*(self: ChessBoard, loc: Location): Piece {.inline.} =
|
|
## Gets the piece at the given location
|
|
return self.grid[loc.row, loc.col]
|
|
|
|
|
|
func getPiece*(self: ChessBoard, square: string): Piece {.inline.} =
|
|
## Gets the piece on the given square
|
|
## in algebraic notation
|
|
return self.getPiece(square.algebraicToLocation())
|
|
|
|
|
|
func isPromotion*(move: Move): bool {.inline.} =
|
|
## Returns whether the given move is a
|
|
## pawn promotion
|
|
for promotion in [PromoteToBishop, PromoteToKnight, PromoteToRook, PromoteToQueen]:
|
|
if (move.flags and promotion.uint16) != 0:
|
|
return true
|
|
|
|
|
|
func getPromotionType*(move: Move): MoveFlag {.inline.} =
|
|
## Returns the promotion type of the given move.
|
|
## The return value of this function is only valid
|
|
## if isPromotion() returns true
|
|
for promotion in [PromoteToBishop, PromoteToKnight, PromoteToRook, PromoteToQueen]:
|
|
if (move.flags and promotion.uint16) != 0:
|
|
return promotion
|
|
|
|
|
|
func isCapture*(move: Move): bool {.inline.} =
|
|
## Returns whether the given move is a
|
|
## cature
|
|
result = (move.flags and Capture.uint16) == Capture.uint16
|
|
|
|
|
|
func isCastling*(move: Move): bool {.inline.} =
|
|
## Returns whether the given move is a
|
|
## castle
|
|
for flag in [CastleLong, CastleShort]:
|
|
if (move.flags and flag.uint16) != 0:
|
|
return true
|
|
|
|
|
|
func getCastlingType*(move: Move): MoveFlag {.inline.} =
|
|
## Returns the castling type of the given move.
|
|
## The return value of this function is only valid
|
|
## if isCastling() returns true
|
|
for flag in [CastleLong, CastleShort]:
|
|
if (move.flags and flag.uint16) != 0:
|
|
return flag
|
|
|
|
|
|
func isEnPassant*(move: Move): bool {.inline.} =
|
|
## Returns whether the given move is an
|
|
## en passant capture
|
|
result = (move.flags and EnPassant.uint16) != 0
|
|
|
|
|
|
func isDoublePush*(move: Move): bool {.inline.} =
|
|
## Returns whether the given move is a
|
|
## double pawn push
|
|
result = (move.flags and DoublePush.uint16) != 0
|
|
|
|
|
|
func getFlags*(move: Move): seq[MoveFlag] =
|
|
## Gets all the flags of this move
|
|
for flag in [EnPassant, Capture, DoublePush, CastleLong, CastleShort,
|
|
PromoteToBishop, PromoteToKnight, PromoteToQueen,
|
|
PromoteToRook]:
|
|
if (move.flags and flag.uint16) == flag.uint16:
|
|
result.add(flag)
|
|
if result.len() == 0:
|
|
result.add(Default)
|
|
|
|
|
|
func getKing(self: ChessBoard, color: PieceColor = None): Location {.inline.} =
|
|
## Returns the location of the king for the given
|
|
## color (if it is None, the active color is used)
|
|
var color = color
|
|
if color == None:
|
|
color = self.getActiveColor()
|
|
case color:
|
|
of White:
|
|
return self.position.pieces.white.king
|
|
of Black:
|
|
return self.position.pieces.black.king
|
|
else:
|
|
discard
|
|
|
|
|
|
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
|
|
var color = color
|
|
if color == None:
|
|
color = self.getActiveColor()
|
|
case color:
|
|
of White:
|
|
result = self.isAttacked(self.position.pieces.white.king, Black)
|
|
of Black:
|
|
result = self.isAttacked(self.position.pieces.black.king, White)
|
|
else:
|
|
# Unreachable
|
|
discard
|
|
|
|
|
|
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 None, the
|
|
## currently active color is used
|
|
var color = color
|
|
if color == None:
|
|
color = self.getActiveColor()
|
|
# Check if castling rights are still available for moving side
|
|
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
|
|
# Some of these checks may seem redundant, but we
|
|
# perform them because they're less expensive
|
|
|
|
# King is not on its starting square
|
|
if self.getKing(color) != getKingStartingPosition(color):
|
|
return (false, false)
|
|
if self.inCheck(color):
|
|
# King can not castle out of check
|
|
return (false, false)
|
|
if result.king or result.queen:
|
|
var
|
|
loc: Location
|
|
queenSide: Location
|
|
kingSide: Location
|
|
# If the path between the king and rook on a given side is blocked, or any of the
|
|
# squares where the king would move to are attacked by the opponent, then castling
|
|
# is temporarily 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
|
|
|
|
# We only need to check for attacked squares up until
|
|
# the point where the king arrives on the target castling
|
|
# square, but we _also_ need to make sure the path is free
|
|
# of obstacles for the rook to move past the king. This variable
|
|
# becomes false once the king has arrived on its target square so
|
|
# that we don't prevent castling when it would otherwise be allowed
|
|
# (for an example see r3k2r/p1pNqpb1/bn2pnp1/3P4/1p2P3/2N2Q1p/PPPBBPPP/R3K2R b KQkq - 0 1)
|
|
var checkAttacks = true
|
|
|
|
if result.king:
|
|
# Short castle
|
|
var
|
|
location = loc
|
|
otherPiece: Piece
|
|
while true:
|
|
location = location + kingSide
|
|
|
|
if location == color.kingSideRook():
|
|
break
|
|
|
|
otherPiece = self.grid[location.row, location.col]
|
|
|
|
if otherPiece.color != None:
|
|
result.king = false
|
|
break
|
|
|
|
if checkAttacks and self.isAttacked(location, color.opposite()):
|
|
result.king = false
|
|
break
|
|
|
|
# King has arrived at the target square: we no longer
|
|
# need to check whether subsequent squares are free from
|
|
# attacks
|
|
if location == shortCastleKing() + loc:
|
|
checkAttacks = false
|
|
|
|
if result.queen:
|
|
checkAttacks = true
|
|
# Long castle
|
|
var
|
|
location = loc
|
|
otherPiece: Piece
|
|
while true:
|
|
location = location + queenSide
|
|
|
|
if location == color.queenSideRook():
|
|
break
|
|
|
|
otherPiece = self.grid[location.row, location.col]
|
|
|
|
if otherPiece.color != None:
|
|
result.queen = false
|
|
break
|
|
|
|
if checkAttacks and self.isAttacked(location, color.opposite()):
|
|
result.queen = false
|
|
break
|
|
|
|
if location == longCastleKing() + loc:
|
|
checkAttacks = false
|
|
|
|
|
|
proc getCheckResolutions(self: ChessBoard, color: PieceColor): seq[Location] =
|
|
## Returns the squares that need to be covered to
|
|
## resolve the current check (including capturing
|
|
## the checking piece). In case of double check, an
|
|
## empty list is returned (as the king must move)
|
|
var king: Location
|
|
case color:
|
|
of White:
|
|
king = self.position.pieces.white.king
|
|
of Black:
|
|
king = self.position.pieces.black.king
|
|
else:
|
|
return
|
|
|
|
let attackers: seq[Location] = self.getAttackers(king, color.opposite())
|
|
if attackers.len() > 1:
|
|
# Double checks require to move the king
|
|
return @[]
|
|
let
|
|
attacker = attackers[0]
|
|
attackerPiece = self.grid[attacker.row, attacker.col]
|
|
|
|
var attack = self.getAttackFor(attacker, king)
|
|
# Capturing the piece resolves the check
|
|
result.add(attacker)
|
|
# Blocking the attack is also a viable strategy
|
|
# (unless the check is from a knight or a pawn,
|
|
# in which case either the king has to move or
|
|
# that piece has to be captured, but this is
|
|
# already implicitly handled by the loop below)
|
|
|
|
var location = attacker
|
|
while location != king:
|
|
location = location + attack.direction
|
|
if not location.isValid():
|
|
break
|
|
result.add(location)
|
|
|
|
|
|
proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
|
|
## Generates the possible moves for the pawn in the given
|
|
## location
|
|
var
|
|
piece = self.grid[location.row, location.col]
|
|
directions: seq[Location] = @[]
|
|
doAssert piece.kind == Pawn, &"generatePawnMoves called on a {piece.kind}"
|
|
# Pawns can move forward one square
|
|
let forward = location + piece.color.topSide()
|
|
# Only if the square is empty though
|
|
if forward.isValid() and self.grid[forward].color == None:
|
|
directions.add(piece.color.topSide())
|
|
# If the pawn is on its first rank, it can push two squares
|
|
if location.row == piece.getStartRow():
|
|
let double = location + piece.color.doublePush()
|
|
# Check that both squares are empty
|
|
if double.isValid() and self.grid[forward].color == None and self.grid[double].color == None:
|
|
directions.add(piece.color.doublePush())
|
|
let enPassantPawn = self.getEnPassantTarget() + piece.color.opposite().topSide()
|
|
# They can also move one square on either of their
|
|
# forward diagonals, but only for captures and en passant
|
|
for diagonal in [piece.color.topRightDiagonal(), piece.color.topLeftDiagonal()]:
|
|
let target = location + diagonal
|
|
if target.isValid():
|
|
let otherPiece = self.grid[target]
|
|
if target == self.position.enPassantSquare and self.grid[enPassantPawn].color == piece.color.opposite():
|
|
# En passant may be possible
|
|
let targetPawn = self.grid[enPassantPawn]
|
|
# Remove both pieces and see if the king ends up in check
|
|
self.removePiece(enPassantPawn, attack=false)
|
|
self.removePiece(location, attack=false)
|
|
self.updateAttackedSquares()
|
|
if not self.inCheck(piece.color):
|
|
# King is not in check after en passant: move is legal
|
|
directions.add(diagonal)
|
|
# Reset what we just did and reupdate the attack metadata
|
|
self.spawnPiece(location, piece)
|
|
self.spawnPiece(enPassantPawn, targetPawn)
|
|
self.updateAttackedSquares()
|
|
elif otherPiece.color == piece.color.opposite() and otherPiece.kind != King: # Can't capture the king!
|
|
# A capture may be possible
|
|
directions.add(diagonal)
|
|
# Check for pins
|
|
let pinned = self.getPinnedDirections(location)
|
|
if pinned.len() > 0:
|
|
var newDirections: seq[Location] = @[]
|
|
for direction in directions:
|
|
if direction in pinned:
|
|
newDirections.add(direction)
|
|
directions = newDirections
|
|
let checked = self.inCheck()
|
|
let resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color)
|
|
var targetPiece: Piece
|
|
for direction in directions:
|
|
let target = location + direction
|
|
if checked and target notin resolutions:
|
|
continue
|
|
targetPiece = self.grid[target]
|
|
var flags: uint16 = Default.uint16
|
|
if targetPiece.color != None:
|
|
flags = flags or Capture.uint16
|
|
elif abs(location.row - target.row) == 2:
|
|
flags = flags or DoublePush.uint16
|
|
elif target == self.getEnPassantTarget():
|
|
flags = flags or EnPassant.uint16
|
|
if target.row == piece.color.getLastRow():
|
|
# Pawn reached the other side of the board: generate all potential piece promotions
|
|
for promotionType in [PromoteToKnight, PromoteToBishop, PromoteToRook, PromoteToQueen]:
|
|
result.add(Move(startSquare: location, targetSquare: target, flags: promotionType.uint16 or flags))
|
|
continue
|
|
result.add(Move(startSquare: location, targetSquare: target, flags: flags))
|
|
|
|
|
|
proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
|
|
## Generates moves for the sliding piece in the given location
|
|
let piece = self.grid[location.row, location.col]
|
|
doAssert piece.kind in [Bishop, Rook, Queen], &"generateSlidingMoves called on a {piece.kind}"
|
|
var directions: seq[Location] = @[]
|
|
|
|
# Only check in the right directions for the chosen piece
|
|
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())
|
|
let pinned = self.getPinnedDirections(location)
|
|
if pinned.len() > 0:
|
|
var newDirections: seq[Location] = @[]
|
|
for direction in directions:
|
|
if direction in pinned:
|
|
newDirections.add(direction)
|
|
directions = newDirections
|
|
let
|
|
checked = self.inCheck()
|
|
resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color)
|
|
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
|
|
if checked and square notin resolutions:
|
|
# We don't always break out of the loop because
|
|
# we might resolve the check later
|
|
if otherPiece.color == None:
|
|
# We can still move in this direction, so maybe
|
|
# the check can be resolved later
|
|
continue
|
|
else:
|
|
# Our movement is blocked, switch to next direction
|
|
break
|
|
if otherPiece.color == piece.color.opposite:
|
|
# Target square contains an enemy piece: capture
|
|
# it and stop going any further
|
|
if otherPiece.kind != King:
|
|
# Can't capture the king
|
|
result.add(Move(startSquare: location, targetSquare: square, flags: Capture.uint16))
|
|
break
|
|
# Target square is empty, keep going
|
|
result.add(Move(startSquare: location, targetSquare: square))
|
|
|
|
|
|
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}"
|
|
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(piece.color)
|
|
if canCastle.queen:
|
|
directions.add(longCastleKing())
|
|
if canCastle.king:
|
|
directions.add(shortCastleKing())
|
|
var flag = Default
|
|
for direction in directions:
|
|
# Step in this direction once
|
|
let square: Location = location + direction
|
|
# End of board reached
|
|
if not square.isValid():
|
|
continue
|
|
if self.isAttacked(square, piece.color.opposite()):
|
|
continue
|
|
if direction == longCastleKing():
|
|
flag = CastleLong
|
|
elif direction == shortCastleKing():
|
|
flag = CastleShort
|
|
else:
|
|
flag = Default
|
|
let otherPiece = self.grid[square.row, square.col]
|
|
if otherPiece.color == piece.color.opposite():
|
|
flag = Capture
|
|
# A friendly piece is in the way, move onto the next direction
|
|
if otherPiece.color == piece.color:
|
|
continue
|
|
# Target square is empty or contains an enemy piece:
|
|
# All good for us!
|
|
result.add(Move(startSquare: location, targetSquare: square, flags: flag.uint16))
|
|
|
|
|
|
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}"
|
|
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)]
|
|
let pinned = self.getPinnedDirections(location)
|
|
if pinned.len() > 0:
|
|
# Knight is pinned: can't move!
|
|
return @[]
|
|
let checked = self.inCheck()
|
|
let resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color)
|
|
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 or the opponent king is is in the way
|
|
if otherPiece.color == piece.color or otherPiece.kind == King:
|
|
continue
|
|
if checked and square notin resolutions:
|
|
continue
|
|
if otherPiece.color != None:
|
|
# Target square contains an enemy piece: capture
|
|
# it
|
|
result.add(Move(startSquare: location, targetSquare: square, flags: Capture.uint16))
|
|
else:
|
|
# Target square is empty
|
|
result.add(Move(startSquare: location, targetSquare: square))
|
|
|
|
|
|
proc generateMoves(self: ChessBoard, location: Location): seq[Move] =
|
|
## Returns the list of possible legal chess moves for the
|
|
## piece in the given location
|
|
let piece = self.grid[location.row, location.col]
|
|
case piece.kind:
|
|
of Queen, Bishop, Rook:
|
|
return self.generateSlidingMoves(location)
|
|
of Pawn:
|
|
return self.generatePawnMoves(location)
|
|
of King:
|
|
return self.generateKingMoves(location)
|
|
of Knight:
|
|
return self.generateKnightMoves(location)
|
|
else:
|
|
return @[]
|
|
|
|
|
|
proc generateAllMoves*(self: ChessBoard): seq[Move] =
|
|
## Returns the list of all possible legal moves
|
|
## in the current position
|
|
for i in 0..7:
|
|
for j in 0..7:
|
|
if self.grid[i, j].color == self.getActiveColor():
|
|
for move in self.generateMoves((int8(i), int8(j))):
|
|
result.add(move)
|
|
|
|
|
|
proc isAttacked*(self: ChessBoard, loc: Location, color: PieceColor = None): bool =
|
|
## Returns whether the given location is attacked
|
|
## by the given color
|
|
var color = color
|
|
if color == None:
|
|
color = self.getActiveColor().opposite()
|
|
case color:
|
|
of Black:
|
|
for attack in self.position.attacked.black:
|
|
if attack.target == loc:
|
|
return true
|
|
of White:
|
|
for attack in self.position.attacked.white:
|
|
if attack.target == loc:
|
|
return true
|
|
of None:
|
|
discard
|
|
|
|
|
|
proc getAttackers*(self: ChessBoard, loc: Location, color: PieceColor): seq[Location] =
|
|
## Returns all the attackers of the given color
|
|
## for the given square
|
|
case color:
|
|
of Black:
|
|
for attack in self.position.attacked.black:
|
|
if attack.target == loc:
|
|
result.add(attack.source)
|
|
of White:
|
|
for attack in self.position.attacked.white:
|
|
if attack.target == loc:
|
|
result.add(attack.source)
|
|
of None:
|
|
discard
|
|
|
|
|
|
proc getAttacks*(self: ChessBoard, loc: Location): Attacked =
|
|
## Returns all the squares attacked by the piece in the given
|
|
## location
|
|
let piece = self.grid[loc.row, loc.col]
|
|
case piece.color:
|
|
of Black:
|
|
for attack in self.position.attacked.black:
|
|
if attack.source == loc:
|
|
result.add(attack)
|
|
of White:
|
|
for attack in self.position.attacked.white:
|
|
if attack.source == loc:
|
|
result.add(attack)
|
|
of None:
|
|
discard
|
|
|
|
|
|
proc getAttackFor*(self: ChessBoard, source, target: Location): tuple[source, target, direction: Location] =
|
|
## Returns the first attack of the piece in the given
|
|
## source location that also attacks the target location
|
|
let piece = self.grid[source.row, source.col]
|
|
case piece.color:
|
|
of Black:
|
|
for attack in self.position.attacked.black:
|
|
if attack.target == target and attack.source == source:
|
|
return attack
|
|
of White:
|
|
for attack in self.position.attacked.white:
|
|
if attack.target == target and attack.source == source:
|
|
return attack
|
|
of None:
|
|
discard
|
|
|
|
|
|
proc isAttacked*(self: ChessBoard, square: string): bool =
|
|
## Returns whether the given square is attacked
|
|
## by the current
|
|
return self.isAttacked(square.algebraicToLocation())
|
|
|
|
|
|
func addAttack(self: ChessBoard, attack: tuple[source, target, direction: Location], color: PieceColor) {.inline.} =
|
|
if attack.source.isValid() and attack.target.isValid():
|
|
case color:
|
|
of White:
|
|
self.position.attacked.white.add(attack)
|
|
of Black:
|
|
self.position.attacked.black.add(attack)
|
|
else:
|
|
discard
|
|
|
|
|
|
proc getPinnedDirections(self: ChessBoard, loc: Location): seq[Location] =
|
|
let piece = self.grid[loc.row, loc.col]
|
|
case piece.color:
|
|
of None:
|
|
discard
|
|
of White:
|
|
for pin in self.position.pinned.black:
|
|
if pin.target == loc:
|
|
result.add(pin.direction)
|
|
of Black:
|
|
for pin in self.position.pinned.white:
|
|
if pin.target == loc:
|
|
result.add(pin.direction)
|
|
|
|
|
|
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)
|
|
self.addAttack((loc, loc + White.topRightDiagonal(), White.topRightDiagonal()), White)
|
|
self.addAttack((loc, loc + White.topLeftDiagonal(), White.topLeftDiagonal()), White)
|
|
# We do the same thing for black
|
|
for loc in self.position.pieces.black.pawns:
|
|
self.addAttack((loc, loc + Black.topRightDiagonal(), Black.topRightDiagonal()), Black)
|
|
self.addAttack((loc, loc + Black.topLeftDiagonal(), Black.topLeftDiagonal()), Black)
|
|
|
|
|
|
proc updateKingAttacks(self: ChessBoard) =
|
|
## Internal helper of updateAttackedSquares
|
|
var king = self.position.pieces.white.king
|
|
self.addAttack((king, king + White.topRightDiagonal(), White.topRightDiagonal()), White)
|
|
self.addAttack((king, king + White.topLeftDiagonal(), White.topLeftDiagonal()), White)
|
|
self.addAttack((king, king + White.bottomLeftDiagonal(), White.bottomLeftDiagonal()), White)
|
|
self.addAttack((king, king + White.bottomRightDiagonal(), White.bottomRightDiagonal()), White)
|
|
king = self.position.pieces.black.king
|
|
self.addAttack((king, king + Black.topRightDiagonal(), Black.topRightDiagonal()), Black)
|
|
self.addAttack((king, king + Black.topLeftDiagonal(), Black.topLeftDiagonal()), Black)
|
|
self.addAttack((king, king + Black.bottomLeftDiagonal(), Black.bottomLeftDiagonal()), Black)
|
|
self.addAttack((king, king + Black.bottomRightDiagonal(), Black.bottomRightDiagonal()), Black)
|
|
|
|
|
|
proc updateKnightAttacks(self: ChessBoard) =
|
|
## Internal helper of updateAttackedSquares
|
|
for loc in self.position.pieces.white.knights:
|
|
self.addAttack((loc, loc + White.topLeftKnightMove(), White.topLeftKnightMove()), White)
|
|
self.addAttack((loc, loc + White.topRightKnightMove(), White.topRightKnightMove()), White)
|
|
self.addAttack((loc, loc + White.bottomLeftKnightMove(), White.bottomLeftKnightMove()), White)
|
|
self.addAttack((loc, loc + White.bottomRightKnightMove(), White.bottomRightKnightMove()), White)
|
|
self.addAttack((loc, loc + White.topLeftKnightMove(long=false), White.topLeftKnightMove(long=false)), White)
|
|
self.addAttack((loc, loc + White.topRightKnightMove(long=false), White.topRightKnightMove(long=false)), White)
|
|
self.addAttack((loc, loc + White.bottomLeftKnightMove(long=false), White.bottomLeftKnightMove(long=false)), White)
|
|
self.addAttack((loc, loc + White.bottomRightKnightMove(long=false), White.bottomRightKnightMove(long=false)), White)
|
|
|
|
for loc in self.position.pieces.black.knights:
|
|
self.addAttack((loc, loc + Black.topLeftKnightMove(), Black.topLeftKnightMove()), Black)
|
|
self.addAttack((loc, loc + Black.topRightKnightMove(), Black.topRightKnightMove()), Black)
|
|
self.addAttack((loc, loc + Black.bottomLeftKnightMove(), Black.bottomLeftKnightMove()), Black)
|
|
self.addAttack((loc, loc + Black.bottomRightKnightMove(), Black.bottomRightKnightMove()), Black)
|
|
self.addAttack((loc, loc + Black.topLeftKnightMove(long=false), Black.topLeftKnightMove(long=false)), Black)
|
|
self.addAttack((loc, loc + Black.topRightKnightMove(long=false), Black.topRightKnightMove(long=false)), Black)
|
|
self.addAttack((loc, loc + Black.bottomLeftKnightMove(long=false), Black.bottomLeftKnightMove(long=false)), Black)
|
|
self.addAttack((loc, loc + Black.bottomRightKnightMove(long=false), Black.bottomRightKnightMove(long=false)), Black)
|
|
|
|
|
|
proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked, pins: Attacked] =
|
|
## Internal helper of updateSlidingAttacks
|
|
var
|
|
directions: seq[Location] = @[]
|
|
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:
|
|
var
|
|
square = loc
|
|
otherPiece: Piece
|
|
# 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]
|
|
# Target square is attacked (even if a friendly piece
|
|
# is present, because in this case we're defending
|
|
# it)
|
|
result.attacks.add((loc, square, direction))
|
|
# Empty square, keep going
|
|
if otherPiece.color == None:
|
|
continue
|
|
if otherPiece.color == piece.color.opposite():
|
|
if otherPiece.kind != King:
|
|
# We found an enemy piece that is not
|
|
# the enemy king. We don't break out
|
|
# immediately because we first want
|
|
# to check if we've pinned it to the king
|
|
var
|
|
otherSquare: Location = square
|
|
behindPiece: Piece
|
|
while true:
|
|
otherSquare = otherSquare + direction
|
|
if not otherSquare.isValid():
|
|
break
|
|
behindPiece = self.grid[otherSquare.row, otherSquare.col]
|
|
if behindPiece.color == None:
|
|
continue
|
|
if behindPiece.color == piece.color.opposite and behindPiece.kind == King:
|
|
# The enemy king is behind this enemy piece: pin it along
|
|
# this axis in both directions
|
|
result.pins.add((loc, square, direction))
|
|
result.pins.add((loc, square, -direction))
|
|
if otherPiece.kind == Pawn and square.row == otherPiece.getStartRow():
|
|
# The pinned piece is a pawn which hasn't moved yet:
|
|
# we allow it to move two squares as well
|
|
if square.col == loc.col:
|
|
# The pawn can only push two squares if it's being pinned from the
|
|
# top
|
|
result.pins.add((loc, square, otherPiece.color.doublePush()))
|
|
else:
|
|
break
|
|
else:
|
|
# Enemy king is here: ensure it cannot move backwards by
|
|
# attacking the square behind it (if one exists and is
|
|
# valid)
|
|
let target = square + direction
|
|
if target.isValid():
|
|
result.attacks.add((loc, target, direction))
|
|
break
|
|
|
|
|
|
proc updateSlidingAttacks(self: ChessBoard) =
|
|
## Internal helper of updateAttackedSquares
|
|
|
|
var data: tuple[attacks: Attacked, pins: Attacked]
|
|
for loc in self.position.pieces.white.bishops:
|
|
data = self.getSlidingAttacks(loc)
|
|
self.position.attacked.white.extend(data.attacks)
|
|
self.position.pinned.white.extend(data.pins)
|
|
for loc in self.position.pieces.white.rooks:
|
|
data = self.getSlidingAttacks(loc)
|
|
self.position.attacked.white.extend(data.attacks)
|
|
self.position.pinned.white.extend(data.pins)
|
|
for loc in self.position.pieces.white.queens:
|
|
data = self.getSlidingAttacks(loc)
|
|
self.position.attacked.white.extend(data.attacks)
|
|
self.position.pinned.white.extend(data.pins)
|
|
for loc in self.position.pieces.black.bishops:
|
|
data = self.getSlidingAttacks(loc)
|
|
self.position.attacked.black.extend(data.attacks)
|
|
self.position.pinned.black.extend(data.pins)
|
|
for loc in self.position.pieces.black.rooks:
|
|
data = self.getSlidingAttacks(loc)
|
|
self.position.attacked.black.extend(data.attacks)
|
|
self.position.pinned.black.extend(data.pins)
|
|
for loc in self.position.pieces.black.queens:
|
|
data = self.getSlidingAttacks(loc)
|
|
self.position.attacked.black.extend(data.attacks)
|
|
self.position.pinned.black.extend(data.pins)
|
|
|
|
|
|
proc updateAttackedSquares(self: ChessBoard) =
|
|
## Updates internal metadata about which squares
|
|
## are attacked
|
|
self.position.attacked.white.setLen(0)
|
|
self.position.attacked.black.setLen(0)
|
|
self.position.pinned.white.setLen(0)
|
|
self.position.pinned.black.setLen(0)
|
|
# Pawns
|
|
self.updatePawnAttacks()
|
|
# Sliding pieces
|
|
self.updateSlidingAttacks()
|
|
# Knights
|
|
self.updateKnightAttacks()
|
|
# Kings
|
|
self.updateKingAttacks()
|
|
|
|
|
|
proc removePiece(self: ChessBoard, location: Location, attack: bool = true) =
|
|
## Removes a piece from the board, updating necessary
|
|
## metadata
|
|
var piece = self.grid[location.row, location.col]
|
|
self.grid[location.row, location.col] = emptyPiece()
|
|
case piece.color:
|
|
of White:
|
|
case piece.kind:
|
|
of Pawn:
|
|
self.position.pieces.white.pawns.delete(self.position.pieces.white.pawns.find(location))
|
|
of Bishop:
|
|
self.position.pieces.white.bishops.delete(self.position.pieces.white.bishops.find(location))
|
|
of Knight:
|
|
self.position.pieces.white.knights.delete(self.position.pieces.white.knights.find(location))
|
|
of Rook:
|
|
self.position.pieces.white.rooks.delete(self.position.pieces.white.rooks.find(location))
|
|
of Queen:
|
|
self.position.pieces.white.queens.delete(self.position.pieces.white.queens.find(location))
|
|
of King:
|
|
doAssert false, "removePiece: attempted to remove the white king"
|
|
else:
|
|
discard
|
|
of Black:
|
|
case piece.kind:
|
|
of Pawn:
|
|
self.position.pieces.black.pawns.delete(self.position.pieces.black.pawns.find(location))
|
|
of Bishop:
|
|
self.position.pieces.black.bishops.delete(self.position.pieces.black.bishops.find(location))
|
|
of Knight:
|
|
self.position.pieces.black.knights.delete(self.position.pieces.black.knights.find(location))
|
|
of Rook:
|
|
self.position.pieces.black.rooks.delete(self.position.pieces.black.rooks.find(location))
|
|
of Queen:
|
|
self.position.pieces.black.queens.delete(self.position.pieces.black.queens.find(location))
|
|
of King:
|
|
doAssert false, "removePiece: attempted to remove the black king"
|
|
else:
|
|
discard
|
|
else:
|
|
discard
|
|
if attack:
|
|
self.updateAttackedSquares()
|
|
|
|
|
|
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
|
|
let piece = self.grid[move.startSquare.row, move.startSquare.col]
|
|
let targetSquare = self.getPiece(move.targetSquare)
|
|
if targetSquare.color != None:
|
|
raise newException(AccessViolationDefect, &"attempted to overwrite a piece! {move}")
|
|
# Update positional metadata
|
|
case piece.color:
|
|
of White:
|
|
case piece.kind:
|
|
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
|
|
self.position.pieces.white.pawns.delete(self.position.pieces.white.pawns.find(move.startSquare))
|
|
self.position.pieces.white.pawns.add(move.targetSquare)
|
|
of Bishop:
|
|
self.position.pieces.white.bishops.delete(self.position.pieces.white.bishops.find(move.startSquare))
|
|
self.position.pieces.white.bishops.add(move.targetSquare)
|
|
of Knight:
|
|
self.position.pieces.white.knights.delete(self.position.pieces.white.knights.find(move.startSquare))
|
|
self.position.pieces.white.knights.add(move.targetSquare)
|
|
of Rook:
|
|
self.position.pieces.white.rooks.delete(self.position.pieces.white.rooks.find(move.startSquare))
|
|
self.position.pieces.white.rooks.add(move.targetSquare)
|
|
of Queen:
|
|
self.position.pieces.white.queens.delete(self.position.pieces.white.queens.find(move.startSquare))
|
|
self.position.pieces.white.queens.add(move.targetSquare)
|
|
of King:
|
|
self.position.pieces.white.king = move.targetSquare
|
|
else:
|
|
discard
|
|
of Black:
|
|
case piece.kind:
|
|
of Pawn:
|
|
self.position.pieces.black.pawns.delete(self.position.pieces.black.pawns.find(move.startSquare))
|
|
self.position.pieces.black.pawns.add(move.targetSquare)
|
|
of Bishop:
|
|
self.position.pieces.black.bishops.delete(self.position.pieces.black.bishops.find(move.startSquare))
|
|
self.position.pieces.black.bishops.add(move.targetSquare)
|
|
of Knight:
|
|
self.position.pieces.black.knights.delete(self.position.pieces.black.knights.find(move.startSquare))
|
|
self.position.pieces.black.knights.add(move.targetSquare)
|
|
of Rook:
|
|
self.position.pieces.black.rooks.delete(self.position.pieces.black.rooks.find(move.startSquare))
|
|
self.position.pieces.black.rooks.add(move.targetSquare)
|
|
of Queen:
|
|
self.position.pieces.black.queens.delete(self.position.pieces.black.queens.find(move.startSquare))
|
|
self.position.pieces.black.queens.add(move.targetSquare)
|
|
of King:
|
|
self.position.pieces.black.king = move.targetSquare
|
|
else:
|
|
discard
|
|
else:
|
|
discard
|
|
# Empty out the starting square
|
|
self.grid[move.startSquare.row, move.startSquare.col] = emptyPiece()
|
|
# Actually move the piece on the board
|
|
self.grid[move.targetSquare.row, move.targetSquare.col] = 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), attack)
|
|
|
|
|
|
proc doMove(self: ChessBoard, move: Move) =
|
|
## Internal function called by makeMove after
|
|
## performing legality checks. Can be used in
|
|
## performance-critical paths where a move is
|
|
## already known to be legal
|
|
|
|
# Record final position for future reference
|
|
self.positions.add(self.position)
|
|
|
|
# Final checks
|
|
let piece = self.grid[move.startSquare.row, move.startSquare.col]
|
|
|
|
var
|
|
halfMoveClock = self.position.halfMoveClock
|
|
fullMoveCount = self.position.fullMoveCount
|
|
castlingAvailable = self.position.castlingAvailable
|
|
enPassantTarget = emptyLocation()
|
|
# Needed to detect draw by the 50 move rule
|
|
if piece.kind == Pawn or move.isCapture():
|
|
halfMoveClock = 0
|
|
else:
|
|
inc(halfMoveClock)
|
|
if piece.color == Black:
|
|
inc(fullMoveCount)
|
|
|
|
if move.isDoublePush():
|
|
enPassantTarget = move.targetSquare + piece.color.bottomSide()
|
|
|
|
# Castling check: have the rooks moved?
|
|
if piece.kind == Rook:
|
|
case piece.color:
|
|
of White:
|
|
if move.startSquare.row == 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 == 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 move.isCapture():
|
|
let captured = self.grid[move.targetSquare.row, move.targetSquare.col]
|
|
if captured.kind == Rook:
|
|
case captured.color:
|
|
of White:
|
|
if move.targetSquare == captured.color.queenSideRook():
|
|
# Queen side
|
|
castlingAvailable.white.queen = false
|
|
elif move.targetSquare == captured.color.kingSideRook():
|
|
# King side
|
|
castlingAvailable.white.king = false
|
|
of Black:
|
|
if move.targetSquare == captured.color.queenSideRook():
|
|
# Queen side
|
|
castlingAvailable.black.queen = false
|
|
elif move.targetSquare == captured.color.kingSideRook():
|
|
# King side
|
|
castlingAvailable.black.king = false
|
|
else:
|
|
# Unreachable
|
|
discard
|
|
# Has the king moved?
|
|
if piece.kind == King or move.isCastling():
|
|
# Revoke all castling rights for the moving king
|
|
case piece.color:
|
|
of White:
|
|
castlingAvailable.white.king = false
|
|
castlingAvailable.white.queen = false
|
|
of Black:
|
|
castlingAvailable.black.king = false
|
|
castlingAvailable.black.queen = false
|
|
else:
|
|
discard
|
|
|
|
# Create new position
|
|
self.position = Position(plyFromRoot: self.position.plyFromRoot + 1,
|
|
halfMoveClock: halfMoveClock,
|
|
fullMoveCount: fullMoveCount,
|
|
turn: self.getActiveColor().opposite,
|
|
castlingAvailable: castlingAvailable,
|
|
pieces: self.position.pieces,
|
|
enPassantSquare: enPassantTarget
|
|
)
|
|
# Update position metadata
|
|
|
|
if move.isCastling():
|
|
# Move the rook onto the
|
|
# correct file when castling
|
|
var
|
|
location: Location
|
|
target: Location
|
|
flags: uint16
|
|
if move.getCastlingType() == CastleShort:
|
|
location = piece.color.kingSideRook()
|
|
target = shortCastleRook()
|
|
flags = flags or CastleShort.uint16
|
|
else:
|
|
location = piece.color.queenSideRook()
|
|
target = longCastleRook()
|
|
flags = flags or CastleLong.uint16
|
|
let rook = self.grid[location.row, location.col]
|
|
let move = Move(startSquare: location, targetSquare: location + target, flags: flags)
|
|
self.movePiece(move, attack=false)
|
|
|
|
if move.isEnPassant():
|
|
# Make the en passant pawn disappear
|
|
self.removePiece(move.targetSquare + piece.color.bottomSide(), attack=false)
|
|
|
|
|
|
if move.isCapture():
|
|
# Get rid of captured pieces
|
|
self.removePiece(move.targetSquare, attack=false)
|
|
# Move the piece to its target square and update attack metadata
|
|
self.movePiece(move, attack=false)
|
|
if move.isPromotion():
|
|
# Move is a pawn promotion: get rid of the pawn
|
|
# and spawn a new piece
|
|
case move.getPromotionType():
|
|
of PromoteToBishop:
|
|
self.spawnPiece(move.targetSquare, Piece(kind: Bishop, color: piece.color))
|
|
of PromoteToKnight:
|
|
self.spawnPiece(move.targetSquare, Piece(kind: Knight, color: piece.color))
|
|
of PromoteToRook:
|
|
self.spawnPiece(move.targetSquare, Piece(kind: Rook, color: piece.color))
|
|
of PromoteToQueen:
|
|
self.spawnPiece(move.targetSquare, Piece(kind: Queen, color: piece.color))
|
|
else:
|
|
discard
|
|
self.updateAttackedSquares()
|
|
# TODO: Remove this, once I figure out what the heck is wrong
|
|
# with updating the board representation
|
|
self.updateBoard()
|
|
|
|
|
|
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)
|
|
of King:
|
|
doAssert false, "attempted to spawn a white king"
|
|
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)
|
|
of King:
|
|
doAssert false, "attempted to spawn a black king"
|
|
else:
|
|
discard
|
|
else:
|
|
# Unreachable
|
|
discard
|
|
self.grid[location.row, location.col] = piece
|
|
|
|
|
|
proc updateBoard*(self: ChessBoard) =
|
|
## Updates the internal grid representation
|
|
## according to the positional data stored
|
|
## in the chessboard
|
|
for i in 0..63:
|
|
self.grid[i] = emptyPiece()
|
|
for loc in self.position.pieces.white.pawns:
|
|
self.grid[loc.row, loc.col] = Piece(color: White, kind: Pawn)
|
|
for loc in self.position.pieces.black.pawns:
|
|
self.grid[loc.row, loc.col] = Piece(color: Black, kind: Pawn)
|
|
for loc in self.position.pieces.white.bishops:
|
|
self.grid[loc.row, loc.col] = Piece(color: White, kind: Bishop)
|
|
for loc in self.position.pieces.black.bishops:
|
|
self.grid[loc.row, loc.col] = Piece(color: Black, kind: Bishop)
|
|
for loc in self.position.pieces.white.knights:
|
|
self.grid[loc.row, loc.col] = Piece(color: White, kind: Knight)
|
|
for loc in self.position.pieces.black.knights:
|
|
self.grid[loc.row, loc.col] = Piece(color: Black, kind: Knight)
|
|
for loc in self.position.pieces.white.rooks:
|
|
self.grid[loc.row, loc.col] = Piece(color: White, kind: Rook)
|
|
for loc in self.position.pieces.black.rooks:
|
|
self.grid[loc.row, loc.col] = Piece(color: Black, kind: Rook)
|
|
for loc in self.position.pieces.white.queens:
|
|
self.grid[loc.row, loc.col] = Piece(color: White, kind: Queen)
|
|
for loc in self.position.pieces.black.queens:
|
|
self.grid[loc.row, loc.col] = Piece(color: Black, kind: Queen)
|
|
self.grid[self.position.pieces.white.king.row, self.position.pieces.white.king.col] = Piece(color: White, kind: King)
|
|
self.grid[self.position.pieces.black.king.row, self.position.pieces.black.king.col] = Piece(color: Black, kind: King)
|
|
|
|
|
|
proc undoLastMove*(self: ChessBoard) =
|
|
if self.positions.len() > 0:
|
|
self.position = self.positions.pop()
|
|
self.updateBoard()
|
|
|
|
|
|
proc isLegal(self: ChessBoard, move: Move): bool {.inline.} =
|
|
## Returns whether the given move is legal
|
|
return move in self.generateMoves(move.startSquare)
|
|
|
|
|
|
proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} =
|
|
## Makes a move on the board
|
|
result = move
|
|
if not self.isLegal(move):
|
|
return emptyMove()
|
|
self.doMove(move)
|
|
|
|
|
|
proc toChar*(piece: Piece): char =
|
|
if piece.color == White:
|
|
return char(piece.kind).toUpperAscii()
|
|
return char(piece.kind)
|
|
|
|
|
|
proc `$`*(self: ChessBoard): string =
|
|
result &= "- - - - - - - -"
|
|
for i in 0..7:
|
|
result &= "\n"
|
|
for j in 0..7:
|
|
let piece = self.grid[i, j]
|
|
if piece.kind == Empty:
|
|
result &= "x "
|
|
continue
|
|
result &= &"{piece.toChar()} "
|
|
result &= &"{rankToColumn(i + 1) + 1}"
|
|
result &= "\n- - - - - - - -"
|
|
result &= "\na b c d e f g h"
|
|
|
|
|
|
proc toPretty*(piece: Piece): string =
|
|
case piece.color:
|
|
of White:
|
|
case piece.kind:
|
|
of King:
|
|
return "\U2654"
|
|
of Queen:
|
|
return "\U2655"
|
|
of Rook:
|
|
return "\U2656"
|
|
of Bishop:
|
|
return "\U2657"
|
|
of Knight:
|
|
return "\U2658"
|
|
of Pawn:
|
|
return "\U2659"
|
|
else:
|
|
discard
|
|
of Black:
|
|
case piece.kind:
|
|
of King:
|
|
return "\U265A"
|
|
of Queen:
|
|
return "\U265B"
|
|
of Rook:
|
|
return "\U265C"
|
|
of Bishop:
|
|
return "\U265D"
|
|
of Knight:
|
|
return "\U265E"
|
|
of Pawn:
|
|
return "\240\159\168\133"
|
|
else:
|
|
discard
|
|
else:
|
|
discard
|
|
|
|
|
|
proc pretty*(self: ChessBoard): string =
|
|
## Returns a colored version of the
|
|
## board for easier visualization
|
|
for i in 0..7:
|
|
if i > 0:
|
|
result &= "\n"
|
|
for j in 0..7:
|
|
# Equivalent to (i + j) mod 2
|
|
# (I'm just evil)
|
|
if ((i + j) and 1) == 0:
|
|
result &= "\x1b[39;44;1m"
|
|
else:
|
|
result &= "\x1b[39;40;1m"
|
|
let piece = self.grid[i, j]
|
|
if piece.kind == Empty:
|
|
result &= " \x1b[0m"
|
|
else:
|
|
result &= &"{piece.toPretty()} \x1b[0m"
|
|
result &= &" \x1b[33;1m{rankToColumn(i + 1) + 1}\x1b[0m"
|
|
|
|
result &= "\n\x1b[31;1ma b c d e f g h"
|
|
result &= "\x1b[0m"
|
|
|
|
|
|
|
|
proc toFEN*(self: ChessBoard): string =
|
|
## Returns a FEN string of the current
|
|
## position in the chessboard
|
|
var skip: int
|
|
# Piece placement data
|
|
for i in 0..7:
|
|
skip = 0
|
|
for j in 0..7:
|
|
let piece = self.grid[i, j]
|
|
if piece.kind == Empty:
|
|
inc(skip)
|
|
elif skip > 0:
|
|
result &= &"{skip}{piece.toChar()}"
|
|
skip = 0
|
|
else:
|
|
result &= piece.toChar()
|
|
if skip > 0:
|
|
result &= $skip
|
|
if i < 7:
|
|
result &= "/"
|
|
result &= " "
|
|
# Active color
|
|
result &= (if self.getActiveColor() == White: "w" else: "b")
|
|
result &= " "
|
|
# Castling availability
|
|
let castleWhite = self.position.castlingAvailable.white
|
|
let castleBlack = self.position.castlingAvailable.black
|
|
if not (castleBlack.king or castleBlack.queen or castleWhite.king or castleWhite.queen):
|
|
result &= "-"
|
|
else:
|
|
if castleWhite.king:
|
|
result &= "K"
|
|
if castleWhite.queen:
|
|
result &= "Q"
|
|
if castleBlack.king:
|
|
result &= "k"
|
|
if castleBlack.queen:
|
|
result &= "q"
|
|
result &= " "
|
|
# En passant target
|
|
if self.getEnPassantTarget() == emptyLocation():
|
|
result &= "-"
|
|
else:
|
|
result &= self.getEnPassantTarget().locationToAlgebraic()
|
|
result &= " "
|
|
# Halfmove clock
|
|
result &= $self.getHalfMoveCount()
|
|
result &= " "
|
|
# Fullmove number
|
|
result &= $self.getMoveCount()
|
|
|
|
|
|
proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = false, bulk: bool = false): CountData =
|
|
## Counts (and debugs) the number of legal positions reached after
|
|
## the given number of ply
|
|
|
|
let moves = self.generateAllMoves()
|
|
if not bulk:
|
|
if len(moves) == 0 and self.inCheck():
|
|
result.checkmates = 1
|
|
if ply == 0:
|
|
result.nodes = 1
|
|
return
|
|
elif ply == 1 and bulk:
|
|
if divide:
|
|
var postfix = ""
|
|
for move in moves:
|
|
case move.getPromotionType():
|
|
of PromoteToBishop:
|
|
postfix = "b"
|
|
of PromoteToKnight:
|
|
postfix = "n"
|
|
of PromoteToRook:
|
|
postfix = "r"
|
|
of PromoteToQueen:
|
|
postfix = "q"
|
|
else:
|
|
postfix = ""
|
|
echo &"{move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()}{postfix}: 1"
|
|
if verbose:
|
|
echo ""
|
|
return (uint64(len(moves)), 0, 0, 0, 0, 0, 0)
|
|
|
|
for move in moves:
|
|
if verbose:
|
|
let canCastle = self.canCastle(self.getActiveColor())
|
|
echo &"Ply (from root): {self.position.plyFromRoot}"
|
|
echo &"Move: {move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()}, from ({move.startSquare.row}, {move.startSquare.col}) to ({move.targetSquare.row}, {move.targetSquare.col})"
|
|
echo &"Turn: {self.getActiveColor()}"
|
|
echo &"Piece: {self.grid[move.startSquare.row, move.startSquare.col].kind}"
|
|
echo &"Flags: {move.getFlags()}"
|
|
echo &"In check: {(if self.inCheck(): \"yes\" else: \"no\")}"
|
|
echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
|
|
echo &"Position before move: {self.toFEN()}"
|
|
stdout.write("En Passant target: ")
|
|
if self.getEnPassantTarget() != emptyLocation():
|
|
echo self.getEnPassantTarget().locationToAlgebraic()
|
|
else:
|
|
echo "None"
|
|
echo "\n", self.pretty()
|
|
self.doMove(move)
|
|
if ply == 1:
|
|
if move.isCapture():
|
|
inc(result.captures)
|
|
if move.isCastling():
|
|
inc(result.castles)
|
|
if move.isPromotion():
|
|
inc(result.promotions)
|
|
if move.isEnPassant():
|
|
inc(result.enPassant)
|
|
if self.inCheck():
|
|
# Opponent king is in check
|
|
inc(result.checks)
|
|
if verbose:
|
|
let canCastle = self.canCastle(self.getActiveColor())
|
|
echo "\n"
|
|
echo &"Opponent in check: {(if self.inCheck(): \"yes\" else: \"no\")}"
|
|
echo &"Opponent can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
|
|
echo &"Position after move: {self.toFEN()}"
|
|
echo "\n", self.pretty()
|
|
stdout.write("nextpos>> ")
|
|
try:
|
|
discard readLine(stdin)
|
|
except IOError:
|
|
discard
|
|
except EOFError:
|
|
discard
|
|
let next = self.perft(ply - 1, verbose, bulk=bulk)
|
|
self.undoLastMove()
|
|
if divide and (not bulk or ply > 1):
|
|
var postfix = ""
|
|
if move.isPromotion():
|
|
case move.getPromotionType():
|
|
of PromoteToBishop:
|
|
postfix = "b"
|
|
of PromoteToKnight:
|
|
postfix = "n"
|
|
of PromoteToRook:
|
|
postfix = "r"
|
|
of PromoteToQueen:
|
|
postfix = "q"
|
|
else:
|
|
discard
|
|
echo &"{move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()}{postfix}: {next.nodes}"
|
|
if verbose:
|
|
echo ""
|
|
result.nodes += next.nodes
|
|
result.captures += next.captures
|
|
result.checks += next.checks
|
|
result.promotions += next.promotions
|
|
result.castles += next.castles
|
|
result.enPassant += next.enPassant
|
|
result.checkmates += next.checkmates
|
|
|
|
|
|
proc handleGoCommand(board: ChessBoard, command: seq[string]) =
|
|
if len(command) < 2:
|
|
echo &"Error: go: invalid number of arguments"
|
|
return
|
|
case command[1]:
|
|
of "perft":
|
|
if len(command) == 2:
|
|
echo &"Error: go: perft: invalid number of arguments"
|
|
return
|
|
var
|
|
args = command[2].splitWhitespace()
|
|
bulk = false
|
|
verbose = false
|
|
if args.len() > 1:
|
|
var ok = true
|
|
for arg in args[1..^1]:
|
|
case arg:
|
|
of "bulk":
|
|
bulk = true
|
|
of "verbose":
|
|
verbose = true
|
|
else:
|
|
echo &"Error: go: perft: invalid argument '{args[1]}'"
|
|
ok = false
|
|
break
|
|
if not ok:
|
|
return
|
|
try:
|
|
let ply = parseInt(args[0])
|
|
if bulk:
|
|
let t = cpuTime()
|
|
let nodes = board.perft(ply, divide=true, bulk=true, verbose=verbose).nodes
|
|
echo &"\nNodes searched (bulk-counting: on): {nodes}"
|
|
echo &"Time taken: {round(cpuTime() - t, 3)} seconds\n"
|
|
else:
|
|
let t = cpuTime()
|
|
let data = board.perft(ply, divide=true, verbose=verbose)
|
|
echo &"\nNodes searched (bulk-counting: off): {data.nodes}"
|
|
echo &" - Captures: {data.captures}"
|
|
echo &" - Checks: {data.checks}"
|
|
echo &" - E.P: {data.enPassant}"
|
|
echo &" - Checkmates: {data.checkmates}"
|
|
echo &" - Castles: {data.castles}"
|
|
echo &" - Promotions: {data.promotions}"
|
|
echo ""
|
|
echo &"Time taken: {round(cpuTime() - t, 3)} seconds"
|
|
except ValueError:
|
|
echo "Error: go: perft: invalid depth"
|
|
else:
|
|
echo &"Error: go: unknown subcommand '{command[1]}'"
|
|
|
|
|
|
proc handleMoveCommand(board: ChessBoard, command: seq[string]): Move {.discardable.} =
|
|
if len(command) != 2:
|
|
echo &"Error: move: invalid number of arguments"
|
|
return
|
|
let moveString = command[1]
|
|
if len(moveString) notin 4..5:
|
|
echo &"Error: move: invalid move syntax"
|
|
return
|
|
var
|
|
startSquare: Location
|
|
targetSquare: Location
|
|
flags: uint16
|
|
|
|
try:
|
|
startSquare = moveString[0..1].algebraicToLocation()
|
|
except ValueError:
|
|
echo &"Error: move: invalid start square ({moveString[0..1]})"
|
|
return
|
|
try:
|
|
targetSquare = moveString[2..3].algebraicToLocation()
|
|
except ValueError:
|
|
echo &"Error: move: invalid target square ({moveString[2..3]})"
|
|
return
|
|
|
|
# Since the user tells us just the source and target square of the move,
|
|
# we have to figure out all the flags by ourselves (whether it's a double
|
|
# push, a capture, a promotion, castling, etc.)
|
|
|
|
if board.grid[targetSquare.row, targetSquare.col].kind != Empty:
|
|
flags = flags or Capture.uint16
|
|
|
|
elif board.grid[startSquare.row, startSquare.col].kind == Pawn and abs(startSquare.row - targetSquare.row) == 2:
|
|
flags = flags or DoublePush.uint16
|
|
|
|
if len(moveString) == 5:
|
|
# Promotion
|
|
case moveString[4]:
|
|
of 'b':
|
|
flags = flags or PromoteToBishop.uint16
|
|
of 'n':
|
|
flags = flags or PromoteToKnight.uint16
|
|
of 'q':
|
|
flags = flags or PromoteToQueen.uint16
|
|
of 'r':
|
|
flags = flags or PromoteToRook.uint16
|
|
else:
|
|
echo &"Error: move: invalid promotion type"
|
|
return
|
|
|
|
|
|
var move = Move(startSquare: startSquare, targetSquare: targetSquare, flags: flags)
|
|
if board.getPiece(move.startSquare).kind == King and move.startSquare == board.getActiveColor().getKingStartingPosition():
|
|
if move.targetSquare == move.startSquare + longCastleKing():
|
|
move.flags = move.flags or CastleLong.uint16
|
|
elif move.targetSquare == move.startSquare + shortCastleKing():
|
|
move.flags = move.flags or CastleShort.uint16
|
|
if move.targetSquare == board.getEnPassantTarget():
|
|
move.flags = move.flags or EnPassant.uint16
|
|
result = board.makeMove(move)
|
|
if result == emptyMove():
|
|
echo &"Error: move: {moveString} is illegal"
|
|
|
|
|
|
proc handlePositionCommand(board: var ChessBoard, command: seq[string]) =
|
|
if len(command) < 2:
|
|
echo "Error: position: invalid number of arguments"
|
|
return
|
|
# Makes sure we don't leave the board in an invalid state if
|
|
# some error occurs
|
|
var tempBoard: ChessBoard
|
|
case command[1]:
|
|
of "startpos":
|
|
tempBoard = newDefaultChessboard()
|
|
if command.len() > 2:
|
|
let args = command[2].splitWhitespace()
|
|
if args.len() > 0:
|
|
var i = 0
|
|
while i < args.len():
|
|
case args[i]:
|
|
of "moves":
|
|
var j = i + 1
|
|
while j < args.len():
|
|
if handleMoveCommand(tempBoard, @["move", args[j]]) == emptyMove():
|
|
return
|
|
inc(j)
|
|
inc(i)
|
|
board = tempBoard
|
|
of "fen":
|
|
if len(command) == 2:
|
|
echo &"Current position: {board.toFEN()}"
|
|
return
|
|
var
|
|
args = command[2].splitWhitespace()
|
|
fenString = ""
|
|
stop = 0
|
|
for i, arg in args:
|
|
if arg in ["moves", ]:
|
|
break
|
|
if i > 0:
|
|
fenString &= " "
|
|
fenString &= arg
|
|
inc(stop)
|
|
args = args[stop..^1]
|
|
try:
|
|
tempBoard = newChessboardFromFEN(fenString)
|
|
except ValueError:
|
|
echo &"error: position: {getCurrentExceptionMsg()}"
|
|
return
|
|
if args.len() > 0:
|
|
var i = 0
|
|
while i < args.len():
|
|
case args[i]:
|
|
of "moves":
|
|
var j = i + 1
|
|
while j < args.len():
|
|
if handleMoveCommand(tempBoard, @["move", args[j]]) == emptyMove():
|
|
return
|
|
inc(j)
|
|
inc(i)
|
|
board = tempBoard
|
|
of "print":
|
|
echo board
|
|
of "pretty":
|
|
echo board.pretty()
|
|
else:
|
|
echo &"error: position: unknown subcommand '{command[1]}'"
|
|
return
|
|
|
|
|
|
proc handleUCICommand(board: var ChessBoard, command: seq[string]) =
|
|
echo "id name Nimfish 0.1"
|
|
echo "id author Nocturn9x & Contributors (see LICENSE)"
|
|
# TODO
|
|
echo "uciok"
|
|
|
|
|
|
const HELP_TEXT = """Nimfish help menu:
|
|
- go: Begin a search
|
|
Subcommands:
|
|
- perft <depth> [options]: Run the performance test at the given depth (in ply) and
|
|
print the results
|
|
Options:
|
|
- bulk: Enable bulk-counting (significantly faster, gives less statistics)
|
|
- verbose: Enable move debugging (for each and every move, not recommended on large searches)
|
|
Example: go perft 5 bulk
|
|
- position: Get/set board position
|
|
Subcommands:
|
|
- fen [string]: Set the board to the given fen string if one is provided, or print
|
|
the current position as a FEN string if no arguments are given
|
|
- startpos: Set the board to the starting position
|
|
- pretty: Pretty-print the current position
|
|
- print: Print the current position using ASCII characters only
|
|
Options:
|
|
- moves {moveList}: Perform the given moves (space-separated, all-lowercase)
|
|
in algebraic notation after the position is loaded. This option only applies
|
|
to the "startpos" and "fen" subcommands: it is ignored otherwise
|
|
Examples:
|
|
- position startpos
|
|
- position fen "..." moves a2a3 a7a6
|
|
- clear: Clear the screen
|
|
- move <move>: Perform the given move in algebraic notation
|
|
- castle: Print castling rights for each side
|
|
- check: Print if the current side to move is in check
|
|
- undo, u: Undoes the last move. Can be used in succession
|
|
- turn: Print which side is to move
|
|
- ep: Print the current en passant target
|
|
- pretty: Shorthand for "position pretty"
|
|
- print: Shorthand for "position print"
|
|
- fen: Shorthand for "position fen"
|
|
- pos <args>: Shorthand for "position <args>"
|
|
- get <square>: Get the piece on the given square
|
|
- uci: enter UCI mode (WIP)
|
|
"""
|
|
|
|
|
|
proc main: int =
|
|
## Nimfish's control interface
|
|
echo "Nimfish by nocturn9x (see LICENSE)"
|
|
var
|
|
board = newDefaultChessboard()
|
|
uciMode = false
|
|
while true:
|
|
var
|
|
cmd: seq[string]
|
|
cmdStr: string
|
|
try:
|
|
if not uciMode:
|
|
stdout.write(">>> ")
|
|
stdout.flushFile()
|
|
cmdStr = readLine(stdin).strip(leading=true, trailing=true, chars={'\t', ' '})
|
|
if cmdStr.len() == 0:
|
|
continue
|
|
cmd = cmdStr.splitWhitespace(maxsplit=2)
|
|
|
|
case cmd[0]:
|
|
of "uci":
|
|
handleUCICommand(board, cmd)
|
|
uciMode = true
|
|
of "clear":
|
|
echo "\x1Bc"
|
|
of "help":
|
|
echo HELP_TEXT
|
|
of "go":
|
|
handleGoCommand(board, cmd)
|
|
of "position", "pos":
|
|
handlePositionCommand(board, cmd)
|
|
of "move":
|
|
handleMoveCommand(board, cmd)
|
|
of "pretty", "print", "fen":
|
|
handlePositionCommand(board, @["position", cmd[0]])
|
|
of "undo", "u":
|
|
board.undoLastMove()
|
|
of "turn":
|
|
echo &"Active color: {board.getActiveColor()}"
|
|
of "ep":
|
|
let target = board.getEnPassantTarget()
|
|
if target != emptyLocation():
|
|
echo &"En passant target: {target.locationToAlgebraic()}"
|
|
else:
|
|
echo "En passant target: None"
|
|
of "get":
|
|
if len(cmd) != 2:
|
|
echo "error: get: invalid number of arguments"
|
|
continue
|
|
try:
|
|
echo board.getPiece(cmd[1])
|
|
except ValueError:
|
|
echo "error: get: invalid square"
|
|
continue
|
|
of "castle":
|
|
let canCastle = board.canCastle()
|
|
echo &"Castling rights for {($board.getActiveColor()).toLowerAscii()}:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
|
|
of "check":
|
|
echo &"{board.getActiveColor()} king in check: {(if board.inCheck(): \"yes\" else: \"no\")}"
|
|
else:
|
|
echo &"Unknown command '{cmd[0]}'. Type 'help' for more information."
|
|
except IOError:
|
|
echo ""
|
|
return -1
|
|
except EOFError:
|
|
echo ""
|
|
return 0
|
|
|
|
|
|
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"
|
|
|
|
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"
|
|
|
|
var b = newDefaultChessboard()
|
|
# 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
|
|
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)
|
|
setControlCHook(proc () {.noconv.} = quit(0))
|
|
|
|
quit(main()) |