CPG/src/Chess/board.nim

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