CPG/src/Chess/board.nim

1506 lines
59 KiB
Nim
Raw Normal View History

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