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