2406 lines
98 KiB
Nim
2406 lines
98 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: int8
|
||
# 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: int8
|
||
# 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
|
||
func isLightSquare(a: Location): bool {.inline.} = (a.row + a.col and 2) == 0
|
||
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 inDoubleCheck*(self: ChessBoard, color: PieceColor = None): bool =
|
||
## Returns whether the given color's
|
||
## king is in double 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.getAttackers(self.position.pieces.white.king, Black).len() > 1
|
||
of Black:
|
||
result = self.getAttackers(self.position.pieces.black.king, White).len() > 1
|
||
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).
|
||
## Note that this function does not handle the special
|
||
## case of a friendly pawn being able to capture an enemy
|
||
## pawn that is checking our friendly king via en passant:
|
||
## that is handled internally by generatePawnMoves
|
||
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] = @[]
|
||
assert 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
|
||
enPassantTarget = self.getEnPassantTarget()
|
||
enPassantPawn = enPassantTarget + piece.color.opposite().topSide()
|
||
topLeft = piece.color.topLeftDiagonal()
|
||
topRight = piece.color.topRightDiagonal()
|
||
var enPassantLegal = false
|
||
# They can also move one square on either of their
|
||
# forward diagonals, but only for captures and en passant
|
||
for diagonal in [topRight, topLeft]:
|
||
let target = location + diagonal
|
||
if target.isValid():
|
||
let otherPiece = self.grid[target]
|
||
if target == enPassantTarget and self.grid[enPassantPawn].color == piece.color.opposite():
|
||
# En passant may be possible
|
||
let targetPawn = self.grid[enPassantPawn]
|
||
# Simulate the move and see if the king ends up in check
|
||
self.removePiece(enPassantPawn, attack=false)
|
||
self.removePiece(location, attack=false)
|
||
self.spawnPiece(target, piece)
|
||
self.updateAttackedSquares()
|
||
if not self.inCheck(piece.color):
|
||
# King is not in check after en passant: move is legal
|
||
directions.add(diagonal)
|
||
enPassantLegal = true
|
||
# Reset what we just did and reupdate the attack metadata
|
||
self.removePiece(target, attack=false)
|
||
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()
|
||
var resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color)
|
||
# If the check comes from a pawn and en passant is legal and would capture it,
|
||
# we add that to the list of possible check resolutions
|
||
if checked and enPassantLegal:
|
||
let attackingPawn = self.getAttackFor(enPassantPawn, self.getKing(piece.color))
|
||
if attackingPawn.source == enPassantPawn:
|
||
resolutions.add(enPassantTarget)
|
||
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]
|
||
assert 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]
|
||
assert 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]
|
||
assert 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 checkInsufficientMaterialPieceCount(self: ChessBoard, color: PieceColor): bool =
|
||
## Helper function for checkInsufficientMaterial
|
||
let
|
||
friendlyPawns = self.countPieces(Piece(kind: Pawn, color: color))
|
||
friendlyRooks = self.countPieces(Piece(kind: Rook, color: color))
|
||
friendlyQueens = self.countPieces(Piece(kind: Queen, color: color))
|
||
friendlyKnights = self.countPieces(Piece(kind: Knight, color: color))
|
||
friendlyBishops = self.countPieces(Piece(kind: Bishop, color: color))
|
||
enemyPawns = self.countPieces(Piece(kind: Pawn, color: color.opposite()))
|
||
enemyRooks = self.countPieces(Piece(kind: Rook, color: color.opposite()))
|
||
enemyQueens = self.countPieces(Piece(kind: Queen, color: color.opposite()))
|
||
enemyKnights = self.countPieces(Piece(kind: Knight, color: color.opposite()))
|
||
enemyBishops = self.countPieces(Piece(kind: Bishop, color: color.opposite()))
|
||
if friendlyPawns > 0 or friendlyRooks > 0 or friendlyQueens > 0:
|
||
return false
|
||
if friendlyKnights >= 2:
|
||
return false
|
||
if friendlyKnights + friendlyBishops >= 2:
|
||
return false
|
||
if friendlyKnights >= 1 and (enemyPawns > 0 or enemyRooks > 0 or enemyBishops > 0 or enemyKnights > 0 or enemyQueens > 0):
|
||
return false
|
||
if friendlyBishops >= 1 and (enemyKnights > 0 or enemyPawns > 0):
|
||
return false
|
||
return true
|
||
|
||
|
||
proc checkInsufficientMaterial(self: ChessBoard): bool =
|
||
## Checks if the given position has not enough material for either side to
|
||
## checkmate the enemy king. Note that the criteria as implemented here are
|
||
## not fully compliant with FIDE rules (they just define a draw by insufficient
|
||
## material as "[...] the position is such that the opponent cannot checkmate
|
||
## the player’s king by any possible series of legal moves.", which is really
|
||
## tricky to implement efficiently). For more info see https://www.reddit.com/r/chess/comments/se89db/a_writeup_on_definitions_of_insufficient_material/
|
||
if not (self.checkInsufficientMaterialPieceCount(White) and self.checkInsufficientMaterialPieceCount(Black)):
|
||
return false
|
||
let
|
||
whiteBishops = self.countPieces(Piece(kind: Bishop, color: White))
|
||
blackBishops = self.countPieces(Piece(kind: Bishop, color: Black))
|
||
if blackBishops + whiteBishops >= 2:
|
||
var
|
||
darkSquare = 0
|
||
lightSquare = 0
|
||
for bishop in self.position.pieces.black.bishops:
|
||
if bishop.isLightSquare():
|
||
lightSquare += 1
|
||
else:
|
||
darkSquare += 1
|
||
for bishop in self.position.pieces.white.bishops:
|
||
if bishop.isLightSquare():
|
||
lightSquare += 1
|
||
else:
|
||
darkSquare += 1
|
||
if darkSquare >= 1 and lightSquare >= 1:
|
||
return false
|
||
return true
|
||
|
||
|
||
proc generateMoves(self: ChessBoard, location: Location): seq[Move] =
|
||
## Returns the list of possible legal chess moves for the
|
||
## piece in the given location
|
||
if self.position.halfMoveClock == 100:
|
||
# Draw by 50-move rule
|
||
return @[]
|
||
# TODO: Check for draw by insufficient material
|
||
#[
|
||
if self.checkInsufficientMaterial():
|
||
return @[]
|
||
]#
|
||
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 from the given source to the
|
||
## given target square
|
||
result = (emptyLocation(), emptyLocation(), emptyLocation())
|
||
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] =
|
||
## Returns all the directions along which the piece in the given
|
||
## location is pinned. If the result is non-empty, the piece at
|
||
## the given location is only allowed to move along the directions
|
||
## returned by this function
|
||
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 regularly 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)
|
||
self.addAttack((king, king + White.leftSide(), White.leftSide()), White)
|
||
self.addAttack((king, king + White.rightSide(), White.rightSide()), White)
|
||
self.addAttack((king, king + White.bottomSide(), White.bottomSide()), White)
|
||
self.addAttack((king, king + White.topSide(), White.topSide()), 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)
|
||
self.addAttack((king, king + Black.leftSide(), Black.leftSide()), Black)
|
||
self.addAttack((king, king + Black.rightSide(), Black.rightSide()), Black)
|
||
self.addAttack((king, king + Black.bottomSide(), Black.bottomSide()), Black)
|
||
self.addAttack((king, king + Black.topSide(), Black.topSide()), 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 side (relative to the pawn itself)
|
||
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() or move.isEnPassant():
|
||
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
|
||
self.removePiece(move.targetSquare)
|
||
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:
|
||
# Unreachable
|
||
discard
|
||
self.updateAttackedSquares()
|
||
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
|
||
# TODO: Should we count stalemates?
|
||
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 0
|
||
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()) |