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-10-30 14:46:27 +01:00
2023-03-18 18:14:30 +01:00
type
2023-10-15 16:53:44 +02:00
2023-03-18 18:14:30 +01:00
PieceColor * = enum
2023-10-15 16:53:44 +02:00
## A piece color enumeration
2023-10-18 10:45:54 +02:00
None = 0 'i8 ,
2023-03-18 18:14:30 +01:00
White ,
Black
2023-10-15 16:53:44 +02:00
2023-03-18 18:14:30 +01:00
PieceKind * = enum
2023-10-15 16:53:44 +02:00
## A chess piece enumeration
2023-10-25 22:41:04 +02:00
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-10-15 16:53:44 +02:00
2023-03-18 18:14:30 +01:00
Piece * = object
2023-10-15 16:53:44 +02:00
## A chess piece
2023-03-18 18:14:30 +01:00
color * : PieceColor
kind * : PieceKind
2023-10-15 16:53:44 +02:00
2023-10-16 14:55:43 +02:00
MoveFlag * = enum
## An enumeration of move flags
2023-11-13 09:52:37 +01:00
Default = 0 'u 16 , # 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
2023-10-20 02:23:07 +02:00
# Castling metadata
2023-11-13 09:52:37 +01:00
CastleLong = 8 ,
CastleShort = 16 ,
2023-10-20 02:23:07 +02:00
# Pawn promotion metadata
2023-11-13 09:52:37 +01:00
PromoteToQueen = 32 ,
PromoteToRook = 64 ,
PromoteToBishop = 128 ,
PromoteToKnight = 256
2023-10-16 14:55:43 +02:00
2023-10-25 22:41:04 +02:00
# Useful type aliases
Location * = tuple [ row , col : int8 ]
2023-10-28 02:32:50 +02:00
Attacked = seq [ tuple [ source , target , direction : Location ] ]
2023-10-25 22:41:04 +02:00
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
2023-10-15 16:53:44 +02:00
## A chess move
startSquare * : Location
targetSquare * : Location
2023-11-13 11:03:54 +01:00
flags * : uint16
2023-11-13 09:52:37 +01:00
2023-10-15 16:53:44 +02:00
2023-10-17 15:08:46 +02:00
Position * = ref object
## A chess position
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)
2023-10-31 23:06:27 +01:00
enPassantSquare * : Location
2023-10-12 11:55:12 +02:00
# Locations of all pieces
pieces : tuple [ white : Pieces , black : Pieces ]
2023-10-31 23:06:27 +01:00
# Squares attacked by both sides
2023-10-21 18:19:41 +02:00
attacked : tuple [ white : Attacked , black : Attacked ]
2023-10-31 23:06:27 +01:00
# Pieces pinned by both sides
2023-10-28 02:32:50 +02:00
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)
2023-11-13 09:52:37 +01:00
grid : seq [ 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-10-12 11:55:12 +02:00
2023-03-18 18:14:30 +01:00
2023-11-01 19:07:09 +01:00
# A bunch of simple utility functions
2023-10-15 16:53:44 +02:00
func emptyPiece * : Piece {. inline . } = Piece ( kind : Empty , color : None )
func emptyLocation * : Location {. inline . } = ( - 1 , - 1 )
func opposite * ( c : PieceColor ) : PieceColor {. inline . } = ( if c = = White : Black else : White )
2023-10-17 16:38:43 +02:00
proc algebraicToLocation * ( s : string ) : Location {. inline . }
2023-10-21 18:19:41 +02:00
proc makeMove * ( self : ChessBoard , move : Move ) : Move {. discardable . }
2023-10-28 02:32:50 +02:00
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 )
2023-10-25 22:41:04 +02:00
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 ]
2023-10-28 02:32:50 +02:00
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 )
2023-10-20 02:23:07 +02:00
proc pretty * ( self : ChessBoard ) : string
2023-10-21 18:19:41 +02:00
proc spawnPiece ( self : ChessBoard , location : Location , piece : Piece )
proc updateAttackedSquares ( self : ChessBoard )
2023-10-25 22:41:04 +02:00
proc getPinnedDirections ( self : ChessBoard , loc : Location ) : seq [ Location ]
2023-10-28 02:32:50 +02:00
proc getAttacks * ( self : ChessBoard , loc : Location ) : Attacked
proc getSlidingAttacks ( self : ChessBoard , loc : Location ) : tuple [ attacks : Attacked , pins : Attacked ]
2023-10-30 17:46:06 +01:00
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 )
2023-10-25 22:41:04 +02:00
proc extend [ T ] ( self : var seq [ T ] , other : openarray [ T ] ) {. inline . } =
for x in other :
self . add ( x )
2024-04-08 20:28:31 +02:00
proc resetBoard * ( 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 ) )
2023-10-25 22:41:04 +02:00
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 ) )
2023-10-20 02:23:07 +02:00
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 :
2023-10-20 02:23:07 +02:00
return ( 1 , - 2 )
2023-10-17 22:16:01 +02:00
elif color = = Black :
2023-10-17 12:42:15 +02:00
if long :
2023-10-21 18:19:41 +02:00
return ( - 2 , 1 )
2023-10-17 12:42:15 +02:00
else :
2023-10-21 18:19:41 +02:00
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 :
2023-10-20 02:23:07 +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 :
2023-10-20 02:23:07 +02:00
return ( - 2 , - 1 )
2023-10-17 12:42:15 +02:00
else :
2023-10-20 02:23:07 +02:00
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 :
2023-10-21 18:19:41 +02:00
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
2023-10-28 02:32:50 +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-28 02:32:50 +02:00
func getEnPassantTarget * ( self : ChessBoard ) : Location {. inline . } =
2023-10-17 16:38:43 +02:00
## Returns the current en passant target square
2023-10-31 23:06:27 +01:00
return self . position . enPassantSquare
2023-10-17 16:38:43 +02:00
2023-10-28 02:32:50 +02:00
func getMoveCount * ( self : ChessBoard ) : int {. inline . } =
2023-10-17 17:27:33 +02:00
## 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 . } =
2023-10-17 17:27:33 +02:00
## 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
2023-10-16 15:25:48 +02:00
func getLastRow ( color : PieceColor ) : int {. inline . } =
## Retrieves the location of the last
## row relative to the given color
2023-10-17 10:31:38 +02:00
case color :
of White :
return 0
of Black :
return 7
else :
return - 1
2023-10-16 15:25:48 +02:00
2023-10-12 11:55:12 +02:00
proc newChessboard : ChessBoard =
## Returns a new, empty chessboard
2023-03-18 18:14:30 +01:00
new ( result )
# Turns our flat sequence into an 8x8 grid
2023-11-13 09:52:37 +01:00
result . grid = newSeqOfCap [ Piece ] ( 64 )
2024-04-08 20:28:31 +02:00
for _ in 0 .. 63 :
result . grid . add ( emptyPiece ( ) )
2023-10-17 15:08:46 +02:00
result . position = Position ( attacked : ( @ [ ] , @ [ ] ) ,
2023-10-31 23:06:27 +01:00
enPassantSquare : emptyLocation ( ) ,
2023-10-20 02:23:07 +02:00
turn : White ,
2023-10-25 22:41:04 +02:00
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
2023-10-12 11:55:12 +02:00
2023-11-13 09:52:37 +01:00
func coordToIndex ( row , col : int ) : int {. inline . } = ( row * 8 ) + col
func ` [ ] ` ( self : seq [ Piece ] , row , column : Natural ) : Piece {. inline . } = self [ coordToIndex ( row , column ) ]
proc `[]=` ( self : var seq [ Piece ] , row , column : Natural , piece : Piece ) {. inline . } = self [ coordToIndex ( row , column ) ] = piece
2023-10-12 11:55:12 +02:00
2023-11-01 19:07:09 +01:00
proc newChessboardFromFEN * ( fen : string ) : ChessBoard =
2023-10-12 11:55:12 +02:00
## Initializes a chessboard with the
2023-11-01 19:07:09 +01:00
## position encoded by the given FEN string
2023-10-12 11:55:12 +02:00
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
2023-10-12 11:55:12 +02:00
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 ( ) :
2023-10-12 11:55:12 +02:00
# Piece
2023-03-18 18:14:30 +01:00
of ' r ' , ' n ' , ' b ' , ' q ' , ' k ' , ' p ' :
2023-10-12 11:55:12 +02:00
# We know for a fact these values are in our
# enumeration, so all is good
{. 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 ) )
2023-10-12 11:55:12 +02:00
of Bishop :
2023-10-17 15:08:46 +02:00
result . position . pieces . black . bishops . add ( ( row , column ) )
2023-10-12 11:55:12 +02:00
of Knight :
2023-10-17 15:08:46 +02:00
result . position . pieces . black . knights . add ( ( row , column ) )
2023-10-12 11:55:12 +02:00
of Rook :
2023-10-17 15:08:46 +02:00
result . position . pieces . black . rooks . add ( ( row , column ) )
2023-10-12 11:55:12 +02:00
of Queen :
2023-10-17 15:08:46 +02:00
result . position . pieces . black . queens . add ( ( row , column ) )
2023-10-12 11:55:12 +02:00
of King :
2023-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 )
2023-10-12 11:55:12 +02:00
else :
discard
of White :
case piece . kind :
of Pawn :
2023-10-17 15:08:46 +02:00
result . position . pieces . white . pawns . add ( ( row , column ) )
2023-10-12 11:55:12 +02:00
of Bishop :
2023-10-17 15:08:46 +02:00
result . position . pieces . white . bishops . add ( ( row , column ) )
2023-10-12 11:55:12 +02:00
of Knight :
2023-10-17 15:08:46 +02:00
result . position . pieces . white . knights . add ( ( row , column ) )
2023-10-12 11:55:12 +02:00
of Rook :
2023-10-17 15:08:46 +02:00
result . position . pieces . white . rooks . add ( ( row , column ) )
2023-10-12 11:55:12 +02:00
of Queen :
2023-10-17 15:08:46 +02:00
result . position . pieces . white . queens . add ( ( row , column ) )
2023-10-12 11:55:12 +02:00
of King :
2023-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 )
2023-10-12 11:55:12 +02:00
else :
discard
else :
discard
result . grid [ row , column ] = piece
2023-03-18 18:14:30 +01:00
inc ( column )
of ' / ' :
2023-10-12 11:55:12 +02:00
# Next row
2023-03-18 18:14:30 +01:00
inc ( row )
column = 0
of ' 0 ' .. ' 9 ' :
2023-10-12 11:55:12 +02:00
# Skip x columns
2023-10-20 02:23:07 +02:00
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 ' - ' :
2023-10-12 11:55:12 +02:00
# Field is already uninitialized to the correct state
discard
2023-03-18 18:14:30 +01:00
else :
2023-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 )
2023-10-21 18:19:41 +02:00
result . updateAttackedSquares ( )
2023-10-30 17:46:06 +01:00
if result . inCheck ( result . getActiveColor ( ) . opposite ) :
2023-11-01 19:07:09 +01:00
# Opponent king cannot be captured on the next move
2023-10-30 17:46:06 +01:00
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
2023-10-28 02:32:50 +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 " )
2023-10-15 16:53:44 +02:00
proc countPieces * ( self : ChessBoard , kind : PieceKind , color : PieceColor ) : int =
2023-10-28 02:32:50 +02:00
## Returns the number of pieces with
2024-04-08 20:28:31 +02:00
## the given color and type in the
## current position
2023-10-15 16:53:44 +02:00
case color :
of White :
case kind :
of Pawn :
2023-10-17 15:08:46 +02:00
return self . position . pieces . white . pawns . len ( )
2023-10-15 16:53:44 +02:00
of Bishop :
2023-10-17 15:08:46 +02:00
return self . position . pieces . white . bishops . len ( )
2023-10-15 16:53:44 +02:00
of Knight :
2023-10-17 15:08:46 +02:00
return self . position . pieces . white . knights . len ( )
2023-10-15 16:53:44 +02:00
of Rook :
2023-10-17 15:08:46 +02:00
return self . position . pieces . white . rooks . len ( )
2023-10-15 16:53:44 +02:00
of Queen :
2023-10-17 15:08:46 +02:00
return self . position . pieces . white . queens . len ( )
2023-10-15 16:53:44 +02:00
of King :
# There shall be only one, forever
return 1
else :
2023-10-28 02:32:50 +02:00
raise newException ( ValueError , " invalid piece type " )
2023-10-15 16:53:44 +02:00
of Black :
case kind :
of Pawn :
2023-10-17 15:08:46 +02:00
return self . position . pieces . black . pawns . len ( )
2023-10-15 16:53:44 +02:00
of Bishop :
2023-10-17 15:08:46 +02:00
return self . position . pieces . black . bishops . len ( )
2023-10-15 16:53:44 +02:00
of Knight :
2023-10-17 15:08:46 +02:00
return self . position . pieces . black . knights . len ( )
2023-10-15 16:53:44 +02:00
of Rook :
2023-10-17 15:08:46 +02:00
return self . position . pieces . black . rooks . len ( )
2023-10-15 16:53:44 +02:00
of Queen :
2023-10-17 15:08:46 +02:00
return self . position . pieces . black . queens . len ( )
2023-10-15 16:53:44 +02:00
of King :
2023-10-16 14:55:43 +02:00
# In perpetuity
2023-10-15 16:53:44 +02:00
return 1
else :
2023-10-28 02:32:50 +02:00
raise newException ( ValueError , " invalid piece type " )
2023-10-15 16:53:44 +02:00
of None :
2023-10-28 02:32:50 +02:00
raise newException ( ValueError , " invalid piece color " )
2023-10-15 16:53:44 +02:00
2023-10-28 02:32:50 +02:00
func countPieces * ( self : ChessBoard , piece : Piece ) : int {. inline . } =
2023-10-15 16:53:44 +02:00
## Returns the number of pieces on the board that
2023-10-28 02:32:50 +02:00
## are of the same type and color as the given piece
2023-10-15 16:53:44 +02:00
return self . countPieces ( piece . kind , piece . color )
2023-10-28 02:32:50 +02:00
func rankToColumn ( rank : int ) : int8 {. inline . } =
2023-10-15 16:53:44 +02:00
## 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 ]
2023-10-15 16:53:44 +02:00
return indeces [ rank - 1 ]
2024-04-08 20:28:31 +02:00
func rowToFile ( row : int ) : int {. inline . } =
2023-10-28 02:32:50 +02:00
## Converts a row into our grid into
2024-04-08 20:28:31 +02:00
## a chess file
2023-10-17 17:27:33 +02:00
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 =
2023-10-15 16:53:44 +02:00
## Converts a square location from algebraic
## notation to its corresponding row and column
## in the chess grid (0 indexed)
if len ( s ) ! = 2 :
raise newException ( ValueError , " algebraic position must be of length 2 " )
var s = s . toLowerAscii ( )
if s [ 0 ] notin ' a ' .. ' h ' :
raise newException ( ValueError , & " algebraic position has invalid first character ( ' {s[0]} ' ) " )
if s [ 1 ] notin ' 1 ' .. ' 8 ' :
raise newException ( ValueError , & " algebraic position has invalid second character ( ' {s[1]} ' ) " )
2023-10-18 10:45:54 +02:00
let rank = int8 ( uint8 ( s [ 0 ] ) - uint8 ( ' a ' ) )
2023-10-17 17:27:33 +02:00
# Convert the file character to a number
2023-10-18 10:45:54 +02:00
let file = rankToColumn ( int8 ( uint8 ( s [ 1 ] ) - uint8 ( ' 0 ' ) ) )
2023-10-17 17:27:33 +02:00
return ( file , rank )
2023-10-15 16:53:44 +02:00
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
2023-10-28 02:32:50 +02:00
func getPiece * ( self : ChessBoard , loc : Location ) : Piece {. inline . } =
2023-10-21 18:19:41 +02:00
## Gets the piece at the given location
return self . grid [ loc . row , loc . col ]
2023-10-28 02:32:50 +02:00
func getPiece * ( self : ChessBoard , square : string ) : Piece {. inline . } =
2023-10-15 16:53:44 +02:00
## Gets the piece on the given square
## in algebraic notation
2023-10-21 18:19:41 +02:00
return self . getPiece ( square . algebraicToLocation ( ) )
2023-10-15 16:53:44 +02:00
2023-10-16 22:14:58 +02:00
2023-10-23 18:02:31 +02:00
func isPromotion * ( move : Move ) : bool {. inline . } =
2023-11-13 09:52:37 +01:00
## 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 :
2023-11-13 09:52:37 +01:00
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
2023-11-13 09:52:37 +01:00
## 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 :
2023-11-13 09:52:37 +01:00
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
2023-11-13 09:52:37 +01:00
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 :
2023-11-13 09:52:37 +01:00
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 :
2023-11-13 09:52:37 +01:00
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
2023-11-13 09:52:37 +01:00
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 )
func getKing ( self : ChessBoard , color : PieceColor ) : Location {. inline . } =
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
2023-10-20 02:23:07 +02:00
var color = color
if color = = None :
color = self . getActiveColor ( )
2023-10-17 22:16:01 +02:00
case color :
of White :
2023-10-28 02:32:50 +02:00
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
2023-10-28 02:32:50 +02:00
## 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 )
2023-10-31 23:06:27 +01:00
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
2023-10-17 22:16:01 +02:00
2023-10-23 18:02:31 +02:00
if result . king :
# Short castle
2023-10-25 22:41:04 +02:00
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
2024-04-09 17:46:30 +02:00
2023-11-01 19:07:09 +01:00
if location = = color . kingSideRook ( ) :
break
2024-04-09 17:46:30 +02:00
2023-10-25 22:41:04 +02:00
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 17:46:30 +02:00
if self . isAttacked ( location , color . opposite ( ) ) :
2023-10-23 18:02:31 +02:00
result . king = false
break
2024-04-09 17:46:30 +02:00
2023-10-23 18:02:31 +02:00
if result . queen :
# Long castle
2023-10-25 22:41:04 +02:00
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
2024-04-09 17:46:30 +02:00
2023-11-01 19:07:09 +01:00
if location = = color . queenSideRook ( ) :
break
2024-04-09 17:46:30 +02:00
2023-10-25 22:41:04 +02:00
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 17:46:30 +02:00
if self . isAttacked ( location , color . opposite ( ) ) :
2023-10-28 02:32:50 +02:00
result . queen = false
2023-10-23 18:02:31 +02:00
break
2023-10-28 02:32:50 +02:00
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
2023-10-28 02:32:50 +02:00
## the checking piece). In case of double check, an
## empty list is returned (as the king must move)
var king : Location
case color :
of White :
king = self . position . pieces . white . king
of Black :
king = self . position . pieces . black . king
else :
2023-11-01 19:07:09 +01:00
return
2023-10-28 02:32:50 +02:00
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 ]
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)
2024-04-08 20:28:31 +02:00
2023-11-01 19:07:09 +01:00
var location = attacker
while location ! = king :
location = location + attack . direction
if not location . isValid ( ) :
break
result . add ( location )
2023-10-30 14:46:27 +01:00
2023-10-16 14:55:43 +02:00
proc generatePawnMoves ( self : ChessBoard , location : Location ) : seq [ Move ] =
2023-10-16 15:25:48 +02:00
## Generates the possible moves for the pawn in the given
## location
2023-10-16 14:55:43 +02:00
var
piece = self . grid [ location . row , location . col ]
2024-04-08 20:28:31 +02:00
targets : seq [ Location ] = @ [ ]
2023-10-16 14:55:43 +02:00
doAssert piece . kind = = Pawn , & " generatePawnMoves called on a {piece.kind} "
2023-10-16 22:14:58 +02:00
# Pawns can move forward one square
2023-11-13 11:03:54 +01:00
let forward = piece . color . topSide ( ) + location
2023-10-20 02:23:07 +02:00
# 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 :
2024-04-08 20:28:31 +02:00
targets . add ( forward )
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
2023-10-20 02:23:07 +02:00
if double . isValid ( ) and self . grid [ forward . row , forward . col ] . color = = None and self . grid [ double . row , double . col ] . color = = None :
2024-04-08 20:28:31 +02:00
targets . add ( double )
2023-11-01 19:07:09 +01:00
let enPassantPawn = self . getEnPassantTarget ( ) + piece . color . opposite ( ) . topSide ( )
2024-04-09 17:46:30 +02:00
# They can also move one square on either of their
# forward diagonals, but only for captures and en passant
2023-10-28 02:32:50 +02:00
for diagonal in [ location + piece . color . topRightDiagonal ( ) , location + piece . color . topLeftDiagonal ( ) ] :
if diagonal . isValid ( ) :
2023-11-13 11:03:54 +01:00
let otherPiece = self . grid [ diagonal . row , diagonal . col ]
2024-04-09 17:46:30 +02:00
if diagonal = = self . position . enPassantSquare and self . grid [ enPassantPawn . row , enPassantPawn . col ] . color = = piece . color . opposite ( ) :
2023-10-30 14:46:27 +01:00
# Ensure en passant doesn't create a check
let king = self . getKing ( piece . color )
var ok = true
if king . row = = location . row :
var current = location + piece . color . rightSide ( )
while true :
current = current + piece . color . rightSide ( )
if not current . isValid ( ) :
break
let p = self . grid [ current . row , current . col ]
if p . color = = piece . color :
break
if p . color = = None :
continue
2023-11-13 09:52:37 +01:00
# Bishops can't create checks through en passant
2023-10-30 14:46:27 +01:00
if p . color = = piece . color . opposite ( ) and p . kind in [ Queen , Rook ] :
ok = false
if ok :
2024-04-08 20:28:31 +02:00
targets . add ( diagonal )
2023-11-13 11:03:54 +01:00
elif otherPiece . color = = piece . color . opposite ( ) and otherPiece . kind ! = King :
2024-04-08 20:28:31 +02:00
targets . add ( diagonal )
2023-10-28 02:32:50 +02:00
# Check for pins
2024-04-08 20:28:31 +02:00
let pinned = self . getPinnedDirections ( location )
if pinned . len ( ) > 0 :
var newTargets : seq [ Location ] = @ [ ]
for target in targets :
if target in pinned :
newTargets . add ( target )
targets = newTargets
2023-10-28 02:32:50 +02:00
let checked = self . inCheck ( )
let resolutions = if not checked : @ [ ] else : self . getCheckResolutions ( piece . color )
2023-10-23 18:02:31 +02:00
var targetPiece : Piece
2024-04-08 20:28:31 +02:00
for target in targets :
2023-10-28 02:32:50 +02:00
if checked and target notin resolutions :
continue
2023-10-23 18:02:31 +02:00
targetPiece = self . grid [ target . row , target . col ]
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 ( ) :
2023-10-20 02:23:07 +02:00
# Pawn reached the other side of the board: generate all potential piece promotions
2023-10-16 15:25:48 +02:00
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
2023-10-25 22:41:04 +02:00
let piece = self . grid [ location . row , location . col ]
2023-10-16 14:55:43 +02:00
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 ] = @ [ ]
2023-10-25 22:41:04 +02:00
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 ( ) )
2023-10-25 22:41:04 +02:00
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
2023-10-28 02:32:50 +02:00
let checked = self . inCheck ( )
let 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
2023-10-28 02:32:50 +02:00
if checked and square notin resolutions :
2024-04-08 20:28:31 +02:00
# We don't break out of the loop because
# we might resolve the check later
continue
2023-10-17 10:31:38 +02:00
if otherPiece . color = = piece . color . opposite :
2023-10-17 12:08:07 +02:00
# Target square contains an enemy piece: capture
# it and stop going any further
2023-10-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 ) )
2023-10-17 10:31:38 +02:00
break
# Target square is empty
2023-10-28 02:32:50 +02:00
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 ]
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
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 :
2023-10-20 02:23:07 +02:00
directions . add ( longCastleKing ( ) )
2023-10-17 22:16:01 +02:00
if canCastle . king :
2023-10-20 02:23:07 +02:00
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
2023-10-28 02:32:50 +02:00
if self . isAttacked ( square , piece . color . opposite ( ) ) :
continue
2023-10-20 02:23:07 +02:00
if direction = = longCastleKing ( ) :
2023-10-17 22:16:01 +02:00
flag = CastleLong
2023-10-20 02:23:07 +02:00
elif direction = = shortCastleKing ( ) :
2023-10-17 22:16:01 +02:00
flag = CastleShort
2023-10-20 02:23:07 +02:00
else :
flag = Default
2023-10-17 12:08:07 +02:00
let otherPiece = self . grid [ square . row , square . col ]
2024-04-09 17:46:30 +02:00
if otherPiece . color = = piece . color . opposite ( ) :
2023-10-20 02:23:07 +02:00
flag = Capture
2023-10-28 02:32:50 +02:00
# 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 ]
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-25 22:41:04 +02:00
let pinned = self . getPinnedDirections ( location )
if pinned . len ( ) > 0 :
# Knight is pinned: can't move!
return @ [ ]
2023-10-28 02:32:50 +02:00
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
2023-10-28 02:32:50 +02:00
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
2023-10-28 02:32:50 +02:00
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 ]
2023-10-16 15:25:48 +02:00
case piece . kind :
of Queen , Bishop , Rook :
return self . generateSlidingMoves ( location )
of Pawn :
return self . generatePawnMoves ( location )
2023-10-16 22:14:58 +02:00
of King :
2023-10-17 12:08:07 +02:00
return self . generateKingMoves ( location )
2023-10-17 12:42:15 +02:00
of Knight :
return self . generateKnightMoves ( location )
2023-10-15 16:53:44 +02:00
else :
2023-10-17 12:08:07 +02:00
return @ [ ]
2023-10-15 16:53:44 +02:00
2023-10-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
2023-11-13 09:52:37 +01:00
for i in 0 .. 7 :
for j in 0 .. 7 :
2023-10-21 18:19:41 +02:00
if self . grid [ i , j ] . color = = self . getActiveColor ( ) :
for move in self . generateMoves ( ( int8 ( i ) , int8 ( j ) ) ) :
result . add ( move )
2023-10-28 02:32:50 +02:00
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 =
2023-10-17 10:31:38 +02:00
## Returns whether the given location is attacked
2023-10-25 22:41:04 +02:00
## by the given color
2023-10-23 18:02:31 +02:00
var color = color
if color = = None :
2023-10-25 22:41:04 +02:00
color = self . getActiveColor ( ) . opposite ( )
2023-10-23 18:02:31 +02:00
case color :
2023-10-17 10:31:38 +02:00
of Black :
2023-10-21 18:19:41 +02:00
for attack in self . position . attacked . black :
2023-10-25 22:41:04 +02:00
if attack . target = = loc :
return true
of White :
for attack in self . position . attacked . white :
if attack . target = = loc :
2023-10-17 10:31:38 +02:00
return true
of None :
2023-10-23 18:02:31 +02:00
discard
2023-10-17 10:31:38 +02:00
2023-10-28 02:32:50 +02:00
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 ] =
2024-04-08 20:28:31 +02:00
## Returns the first attack of the piece in the given
2023-10-28 02:32:50 +02:00
## source location that also attacks the target location
let piece = self . grid [ source . row , source . col ]
case piece . color :
of Black :
for attack in self . position . attacked . black :
if attack . target = = target and attack . source = = source :
return attack
of White :
for attack in self . position . attacked . white :
if attack . target = = target and attack . source = = source :
return attack
of None :
discard
2023-10-17 10:31:38 +02:00
proc isAttacked * ( self : ChessBoard , square : string ) : bool =
## Returns whether the given square is attacked
2023-10-25 22:41:04 +02:00
## by the current
2023-10-17 16:38:43 +02:00
return self . isAttacked ( square . algebraicToLocation ( ) )
2023-10-17 10:31:38 +02:00
2023-10-28 02:32:50 +02:00
func addAttack ( self : ChessBoard , attack : tuple [ source , target , direction : Location ] , color : PieceColor ) {. inline . } =
2023-10-25 22:41:04 +02:00
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
2023-10-25 22:41:04 +02:00
proc getPinnedDirections ( self : ChessBoard , loc : Location ) : seq [ Location ] =
let piece = self . grid [ loc . row , loc . col ]
case piece . color :
of None :
discard
of White :
for pin in self . position . pinned . black :
if pin . target = = loc :
result . add ( pin . direction )
of Black :
for pin in self . position . pinned . white :
if pin . target = = loc :
result . add ( pin . direction )
2023-10-28 02:32:50 +02:00
proc updatePawnAttacks ( self : ChessBoard ) =
2023-10-21 18:19:41 +02:00
## 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)
2023-10-28 02:32:50 +02:00
self . addAttack ( ( loc , loc + White . topRightDiagonal ( ) , White . topRightDiagonal ( ) ) , White )
self . addAttack ( ( loc , loc + White . topLeftDiagonal ( ) , White . topRightDiagonal ( ) ) , White )
2023-10-21 18:19:41 +02:00
# We do the same thing for black
for loc in self . position . pieces . black . pawns :
2023-10-28 02:32:50 +02:00
self . addAttack ( ( loc , loc + Black . topRightDiagonal ( ) , Black . topRightDiagonal ( ) ) , Black )
self . addAttack ( ( loc , loc + Black . topLeftDiagonal ( ) , Black . topRightDiagonal ( ) ) , Black )
2023-10-23 18:02:31 +02:00
proc updateKingAttacks ( self : ChessBoard ) =
## Internal helper of updateAttackedSquares
var king = self . position . pieces . white . king
2023-10-28 02:32:50 +02:00
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 )
2023-10-23 18:02:31 +02:00
king = self . position . pieces . black . king
2023-10-28 02:32:50 +02:00
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 )
2023-10-23 18:02:31 +02:00
proc updateKnightAttacks ( self : ChessBoard ) =
## Internal helper of updateAttackedSquares
for loc in self . position . pieces . white . knights :
2023-10-28 02:32:50 +02:00
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 :
2023-10-28 02:32:50 +02:00
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-20 02:23:07 +02:00
2023-10-16 14:55:43 +02:00
2023-10-28 02:32:50 +02:00
proc getSlidingAttacks ( self : ChessBoard , loc : Location ) : tuple [ attacks : Attacked , pins : Attacked ] =
2023-10-21 18:19:41 +02:00
## 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 ( ) )
2023-10-25 22:41:04 +02:00
2023-10-21 18:19:41 +02:00
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 ( ) )
2023-10-25 22:41:04 +02:00
2023-10-21 18:19:41 +02:00
for direction in directions :
2023-10-25 22:41:04 +02:00
var
square = loc
otherPiece : Piece
2023-10-21 18:19:41 +02:00
# 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 ]
2023-10-25 22:41:04 +02:00
# Target square is attacked (even if a friendly piece
# is present, because in this case we're defending
# it)
2023-10-28 02:32:50 +02:00
result . attacks . add ( ( loc , square , direction ) )
2023-10-25 22:41:04 +02:00
# Empty square, keep going
if otherPiece . color = = None :
continue
2023-10-30 17:46:06 +01:00
if otherPiece . color = = piece . color . opposite ( ) :
if otherPiece . kind ! = King :
# We found an enemy piece that is not
# the enemy king. We don't break out
# immediately because we first want
# to check if we've pinned a piece
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 :
2023-10-31 23:06:27 +01:00
# The enemy king is behind this enemy piece: pin it along
# this axis in both directions
result . pins . add ( ( loc , square , direction ) )
2023-10-30 17:46:06 +01:00
result . pins . add ( ( loc , square , - direction ) )
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 ( ) :
2023-10-31 23:06:27 +01:00
result . attacks . add ( ( loc , target , direction ) )
2023-10-25 22:41:04 +02:00
break
2023-10-21 18:19:41 +02:00
proc updateSlidingAttacks ( self : ChessBoard ) =
## Internal helper of updateAttackedSquares
2023-10-28 02:32:50 +02:00
var data : tuple [ attacks : Attacked , pins : Attacked ]
2023-10-17 15:08:46 +02:00
for loc in self . position . pieces . white . bishops :
2023-10-25 22:41:04 +02:00
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 :
2023-10-25 22:41:04 +02:00
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 :
2023-10-25 22:41:04 +02:00
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 )
2023-10-21 18:19:41 +02:00
for loc in self . position . pieces . black . queens :
2023-10-25 22:41:04 +02:00
data = self . getSlidingAttacks ( loc )
self . position . attacked . black . extend ( data . attacks )
self . position . pinned . black . extend ( data . pins )
2023-10-21 18:19:41 +02:00
proc updateAttackedSquares ( self : ChessBoard ) =
## Updates internal metadata about which squares
2023-10-25 22:41:04 +02:00
## are attacked
2023-10-21 18:19:41 +02:00
self . position . attacked . white . setLen ( 0 )
self . position . attacked . black . setLen ( 0 )
# Pawns
self . updatePawnAttacks ( )
# Sliding pieces
self . updateSlidingAttacks ( )
2023-10-25 22:41:04 +02:00
# Knights
2023-10-23 18:02:31 +02:00
self . updateKnightAttacks ( )
# Kings
self . updateKingAttacks ( )
2023-10-16 14:55:43 +02:00
2024-04-08 20:28:31 +02:00
proc removePiece ( self : ChessBoard , location : Location , attack : bool = true , empty : bool = true ) =
2023-10-15 16:53:44 +02:00
## Removes a piece from the board, updating necessary
## metadata
var piece = self . grid [ location . row , location . col ]
2024-04-08 20:28:31 +02:00
if empty :
self . grid [ location . row , location . col ] = emptyPiece ( )
2023-10-15 16:53:44 +02:00
case piece . color :
of White :
case piece . kind :
of Pawn :
2023-10-17 15:08:46 +02:00
self . position . pieces . white . pawns . delete ( self . position . pieces . white . pawns . find ( location ) )
2023-10-15 16:53:44 +02:00
of Bishop :
2024-04-09 17:46:30 +02:00
self . position . pieces . white . bishops . delete ( self . position . pieces . white . bishops . find ( location ) )
2023-10-15 16:53:44 +02:00
of Knight :
2024-04-09 17:46:30 +02:00
self . position . pieces . white . knights . delete ( self . position . pieces . white . knights . find ( location ) )
2023-10-15 16:53:44 +02:00
of Rook :
2023-10-17 15:08:46 +02:00
self . position . pieces . white . rooks . delete ( self . position . pieces . white . rooks . find ( location ) )
2023-10-15 16:53:44 +02:00
of Queen :
2023-10-17 15:08:46 +02:00
self . position . pieces . white . queens . delete ( self . position . pieces . white . queens . find ( location ) )
2023-10-15 16:53:44 +02:00
of King :
doAssert false , " removePiece: attempted to remove the white king "
else :
discard
of Black :
case piece . kind :
of Pawn :
2023-10-20 02:23:07 +02:00
self . position . pieces . black . pawns . delete ( self . position . pieces . black . pawns . find ( location ) )
2023-10-15 16:53:44 +02:00
of Bishop :
2023-10-17 15:08:46 +02:00
self . position . pieces . black . bishops . delete ( self . position . pieces . black . bishops . find ( location ) )
2023-10-15 16:53:44 +02:00
of Knight :
2023-10-17 15:08:46 +02:00
self . position . pieces . black . knights . delete ( self . position . pieces . black . knights . find ( location ) )
2023-10-15 16:53:44 +02:00
of Rook :
2023-10-17 15:08:46 +02:00
self . position . pieces . black . rooks . delete ( self . position . pieces . black . rooks . find ( location ) )
2023-10-15 16:53:44 +02:00
of Queen :
2023-10-17 15:08:46 +02:00
self . position . pieces . black . queens . delete ( self . position . pieces . black . queens . find ( location ) )
2023-10-15 16:53:44 +02:00
of King :
doAssert false , " removePiece: attempted to remove the black king "
else :
discard
else :
discard
2023-10-23 18:02:31 +02:00
if attack :
self . updateAttackedSquares ( )
2023-10-15 16:53:44 +02:00
2023-10-20 02:23:07 +02:00
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
2023-10-28 02:32:50 +02:00
let piece = self . grid [ move . startSquare . row , move . startSquare . col ]
case piece . color :
2023-10-15 16:53:44 +02:00
of White :
2023-10-28 02:32:50 +02:00
case piece . kind :
2023-10-15 16:53:44 +02:00
of Pawn :
# The way things are structured, we don't care about the order
# of this list, so we can add and remove entries as we please
2023-10-17 15:08:46 +02:00
self . position . pieces . white . pawns . delete ( self . position . pieces . white . pawns . find ( move . startSquare ) )
self . position . pieces . white . pawns . add ( move . targetSquare )
2023-10-15 16:53:44 +02:00
of Bishop :
2023-10-17 15:08:46 +02:00
self . position . pieces . white . bishops . delete ( self . position . pieces . white . bishops . find ( move . startSquare ) )
self . position . pieces . white . bishops . add ( move . targetSquare )
2023-10-15 16:53:44 +02:00
of Knight :
2023-10-17 15:08:46 +02:00
self . position . pieces . white . knights . delete ( self . position . pieces . white . knights . find ( move . startSquare ) )
self . position . pieces . white . knights . add ( move . targetSquare )
2023-10-15 16:53:44 +02:00
of Rook :
2023-10-17 15:08:46 +02:00
self . position . pieces . white . rooks . delete ( self . position . pieces . white . rooks . find ( move . startSquare ) )
self . position . pieces . white . rooks . add ( move . targetSquare )
2023-10-15 16:53:44 +02:00
of Queen :
2023-10-17 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 )
2023-10-15 16:53:44 +02:00
of King :
2023-10-17 15:08:46 +02:00
self . position . pieces . white . king = move . targetSquare
2023-10-15 16:53:44 +02:00
else :
2023-10-16 14:55:43 +02:00
discard
2023-10-15 16:53:44 +02:00
of Black :
2023-10-28 02:32:50 +02:00
case piece . kind :
2023-10-15 16:53:44 +02:00
of Pawn :
2023-10-17 15:08:46 +02:00
self . position . pieces . black . pawns . delete ( self . position . pieces . black . pawns . find ( move . startSquare ) )
self . position . pieces . black . pawns . add ( move . targetSquare )
2023-10-15 16:53:44 +02:00
of Bishop :
2023-10-17 15:08:46 +02:00
self . position . pieces . black . bishops . delete ( self . position . pieces . black . bishops . find ( move . startSquare ) )
self . position . pieces . black . bishops . add ( move . targetSquare )
2023-10-15 16:53:44 +02:00
of Knight :
2023-10-17 15:08:46 +02:00
self . position . pieces . black . knights . delete ( self . position . pieces . black . knights . find ( move . startSquare ) )
self . position . pieces . black . knights . add ( move . targetSquare )
2023-10-15 16:53:44 +02:00
of Rook :
2023-10-17 15:08:46 +02:00
self . position . pieces . black . rooks . delete ( self . position . pieces . black . rooks . find ( move . startSquare ) )
self . position . pieces . black . rooks . add ( move . targetSquare )
2023-10-15 16:53:44 +02:00
of Queen :
2023-10-17 15:08:46 +02:00
self . position . pieces . black . queens . delete ( self . position . pieces . black . queens . find ( move . startSquare ) )
self . position . pieces . black . queens . add ( move . targetSquare )
2023-10-15 16:53:44 +02:00
of King :
2023-10-17 15:08:46 +02:00
self . position . pieces . black . king = move . targetSquare
2023-10-15 16:53:44 +02:00
else :
discard
else :
2023-10-17 15:08:46 +02:00
discard
2023-10-21 18:19:41 +02:00
# Empty out the starting square
2023-10-16 14:55:43 +02:00
self . grid [ move . startSquare . row , move . startSquare . col ] = emptyPiece ( )
2023-10-15 16:53:44 +02:00
# Actually move the piece
2023-10-28 02:32:50 +02:00
self . grid [ move . targetSquare . row , move . targetSquare . col ] = piece
2023-10-20 02:23:07 +02:00
if attack :
self . updateAttackedSquares ( )
2023-11-01 19:07:09 +01:00
2023-10-20 02:23:07 +02:00
proc movePiece ( self : ChessBoard , startSquare , targetSquare : Location , attack : bool = true ) =
## Like the other movePiece(), but with two locations
2023-10-28 02:32:50 +02:00
self . movePiece ( Move ( startSquare : startSquare , targetSquare : targetSquare ) , attack )
2023-10-17 15:08:46 +02:00
2024-04-08 20:28:31 +02:00
2023-10-15 16:53:44 +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
2023-10-31 23:06:27 +01:00
# Record final position for future reference
self . positions . add ( self . position )
2023-10-17 15:08:46 +02:00
# Final checks
2023-10-28 02:32:50 +02:00
let piece = self . grid [ move . startSquare . row , move . startSquare . col ]
2023-10-17 22:16:01 +02:00
2023-10-17 17:27:33 +02:00
var
halfMoveClock = self . position . halfMoveClock
fullMoveCount = self . position . fullMoveCount
2023-10-17 22:16:01 +02:00
castlingAvailable = self . position . castlingAvailable
2023-11-01 19:07:09 +01:00
enPassantTarget = self . getEnPassantTarget ( )
# Needed to detect draw by the 50 move rule
2023-11-13 09:52:37 +01:00
if piece . kind = = Pawn or move . isCapture ( ) :
2023-10-31 23:06:27 +01:00
halfMoveClock = 0
2023-10-21 18:19:41 +02:00
else :
inc ( halfMoveClock )
2023-10-28 02:32:50 +02:00
if piece . color = = Black :
2023-10-21 18:19:41 +02:00
inc ( fullMoveCount )
2023-11-01 19:07:09 +01:00
# En passant check
if enPassantTarget ! = emptyLocation ( ) :
let enPassantPawn = enPassantTarget + piece . color . topSide ( )
if self . grid [ enPassantPawn . row , enPassantPawn . col ] . color = = piece . color . opposite ( ) :
enPassantTarget = emptyLocation ( )
2023-11-13 09:52:37 +01:00
if move . isDoublePush ( ) :
2023-11-01 19:07:09 +01:00
enPassantTarget = move . targetSquare + piece . color . bottomSide ( )
2023-10-21 18:19:41 +02:00
# Castling check: have the rooks moved?
2023-10-28 02:32:50 +02:00
if piece . kind = = Rook :
case piece . color :
2023-10-21 18:19:41 +02:00
of White :
2023-10-28 02:32:50 +02:00
if move . startSquare . row = = piece . getStartRow ( ) :
2023-10-21 18:19:41 +02:00
if move . startSquare . col = = 0 :
# Queen side
castlingAvailable . white . queen = false
elif move . startSquare . col = = 7 :
# King side
castlingAvailable . white . king = false
of Black :
2023-10-28 02:32:50 +02:00
if move . startSquare . row = = piece . getStartRow ( ) :
2023-10-21 18:19:41 +02:00
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?
2023-11-13 09:52:37 +01:00
if move . isCapture ( ) :
2023-10-31 23:06:27 +01:00
let captured = self . grid [ move . targetSquare . row , move . targetSquare . col ]
if captured . kind = = Rook :
2024-04-09 17:46:30 +02:00
case captured . color :
2023-10-17 23:56:26 +02:00
of White :
2023-10-31 23:06:27 +01:00
if move . targetSquare = = captured . color . queenSideRook ( ) :
2023-10-21 18:19:41 +02:00
# Queen side
castlingAvailable . white . queen = false
2023-10-31 23:06:27 +01:00
elif move . targetSquare = = captured . color . kingSideRook ( ) :
2023-10-21 18:19:41 +02:00
# King side
castlingAvailable . white . king = false
2023-10-17 23:56:26 +02:00
of Black :
2023-10-31 23:06:27 +01:00
if move . targetSquare = = captured . color . queenSideRook ( ) :
2023-10-21 18:19:41 +02:00
# Queen side
castlingAvailable . black . queen = false
2023-10-31 23:06:27 +01:00
elif move . targetSquare = = captured . color . kingSideRook ( ) :
2023-10-21 18:19:41 +02:00
# King side
castlingAvailable . black . king = false
2023-10-17 23:56:26 +02:00
else :
2023-10-21 18:19:41 +02:00
# Unreachable
2023-10-17 23:56:26 +02:00
discard
2023-10-21 18:19:41 +02:00
# Has the king moved?
2023-11-13 09:52:37 +01:00
if piece . kind = = King or move . isCastling ( ) :
2023-10-28 02:32:50 +02:00
# Revoke all castling rights for the moving king
case piece . color :
2023-10-21 18:19:41 +02:00
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 :
2023-10-21 18:19:41 +02:00
discard
2024-04-08 20:28:31 +02:00
2023-10-21 18:19:41 +02:00
# Create new position
self . position = Position ( plyFromRoot : self . position . plyFromRoot + 1 ,
2023-10-31 23:06:27 +01:00
halfMoveClock : halfMoveClock ,
fullMoveCount : fullMoveCount ,
turn : self . getActiveColor ( ) . opposite ,
castlingAvailable : castlingAvailable ,
pieces : self . position . pieces ,
2023-11-01 19:07:09 +01:00
enPassantSquare : enPassantTarget
2023-10-21 18:19:41 +02:00
)
2023-10-31 23:06:27 +01:00
# Update position metadata
2023-11-13 09:52:37 +01:00
if move . isCastling ( ) :
2023-10-21 18:19:41 +02:00
# Move the rook onto the
2023-10-31 23:06:27 +01:00
# correct file when castling
2023-10-21 18:19:41 +02:00
var
location : Location
target : Location
2024-04-08 20:28:31 +02:00
flags : uint16
2023-11-13 09:52:37 +01:00
if move . getCastlingType ( ) = = CastleShort :
2023-10-28 02:32:50 +02:00
location = piece . color . kingSideRook ( )
2023-10-21 18:19:41 +02:00
target = shortCastleRook ( )
2024-04-08 20:28:31 +02:00
flags = flags or CastleShort . uint16
2023-10-21 18:19:41 +02:00
else :
2023-10-28 02:32:50 +02:00
location = piece . color . queenSideRook ( )
2023-10-21 18:19:41 +02:00
target = longCastleRook ( )
2024-04-08 20:28:31 +02:00
flags = flags or CastleLong . uint16
2023-10-21 18:19:41 +02:00
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 )
2023-10-21 18:19:41 +02:00
self . movePiece ( move , attack = false )
2023-10-17 22:16:01 +02:00
2023-11-13 09:52:37 +01:00
if move . isEnPassant ( ) :
2023-10-31 23:06:27 +01:00
# Make the en passant pawn disappear
self . removePiece ( move . targetSquare + piece . color . bottomSide ( ) , attack = false )
2024-04-08 20:28:31 +02:00
2023-10-31 23:06:27 +01:00
if move . isPromotion ( ) :
2023-10-30 14:46:27 +01:00
# Move is a pawn promotion: get rid of the pawn
# and spawn a new piece
self . removePiece ( move . targetSquare , attack = false )
2023-11-13 09:52:37 +01:00
case move . getPromotionType ( ) :
2023-10-30 14:46:27 +01:00
of PromoteToBishop :
self . spawnPiece ( move . targetSquare , Piece ( kind : Bishop , color : piece . color ) )
of PromoteToKnight :
self . spawnPiece ( move . targetSquare , Piece ( kind : Knight , color : piece . color ) )
of PromoteToRook :
self . spawnPiece ( move . targetSquare , Piece ( kind : Rook , color : piece . color ) )
of PromoteToQueen :
self . spawnPiece ( move . targetSquare , Piece ( kind : Queen , color : piece . color ) )
else :
discard
2023-10-31 23:06:27 +01:00
2024-04-08 20:28:31 +02:00
if move . isCapture ( ) :
# Get rid of captured pieces
self . removePiece ( move . targetSquare , attack = false , empty = false )
# Move the piece to its target square and update attack metadata
self . movePiece ( move )
# TODO: Remove this, once I figure out what the heck is wrong
# with updating the board representation
self . resetBoard ( )
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
2023-10-23 18:02:31 +02:00
proc resetBoard * ( self : ChessBoard ) =
## Resets the internal grid representation
## according to the positional data stored
2024-04-08 20:28:31 +02:00
## in the chessboard
2023-11-13 09:52:37 +01:00
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 )
2023-10-31 23:06:27 +01:00
proc undoLastMove * ( self : ChessBoard ) =
2024-04-08 20:28:31 +02:00
if self . positions . len ( ) > 0 :
self . position = self . positions . pop ( )
2023-10-31 23:06:27 +01:00
self . resetBoard ( )
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-11-13 09:52:37 +01:00
if self . grid [ move . startSquare . row , move . startSquare . col ] . color ! = self . getActiveColor ( ) :
return false
2023-10-23 18:02:31 +02:00
return move in self . generateMoves ( move . startSquare )
2023-10-17 10:31:38 +02:00
proc makeMove * ( self : ChessBoard , move : Move ) : Move {. discardable . } =
2023-10-28 02:32:50 +02:00
## Makes a move on the board
2023-10-17 10:31:38 +02:00
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 )
2023-10-20 02:23:07 +02:00
2023-11-13 09:52:37 +01:00
proc toChar * ( piece : Piece ) : char =
if piece . color = = White :
return char ( piece . kind ) . toUpperAscii ( )
return char ( piece . kind )
2023-10-15 16:53:44 +02:00
proc `$` * ( self : ChessBoard ) : string =
result & = " - - - - - - - - "
2023-11-13 09:52:37 +01:00
for i in 0 .. 7 :
2023-10-15 16:53:44 +02:00
result & = " \n "
2023-11-13 09:52:37 +01:00
for j in 0 .. 7 :
let piece = self . grid [ i , j ]
2023-10-15 16:53:44 +02:00
if piece . kind = = Empty :
2023-10-15 22:46:22 +02:00
result & = " x "
2023-10-15 16:53:44 +02:00
continue
2023-11-13 09:52:37 +01:00
result & = & " {piece.toChar()} "
2023-10-15 16:53:44 +02:00
result & = & " {rankToColumn(i + 1) + 1} "
result & = " \n - - - - - - - - "
result & = " \n a b c d e f g h "
2023-11-13 09:52:37 +01:00
proc toPretty * ( piece : Piece ) : string =
case piece . color :
2024-04-09 17:46:30 +02:00
of White :
2023-11-13 09:52:37 +01:00
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
2024-04-09 17:46:30 +02:00
of Black :
2023-11-13 09:52:37 +01:00
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 :
2024-04-09 17:46:30 +02:00
return " \240 \159 \168 \133 "
2023-11-13 09:52:37 +01:00
else :
discard
else :
discard
2023-10-28 02:32:50 +02:00
2023-10-15 22:46:22 +02:00
proc pretty * ( self : ChessBoard ) : string =
2024-04-09 17:46:30 +02:00
## Returns a colored version of the
2023-10-15 22:46:22 +02:00
## board for easier visualization
2023-11-13 09:52:37 +01:00
for i in 0 .. 7 :
if i > 0 :
result & = " \n "
for j in 0 .. 7 :
2024-04-09 17:46:30 +02:00
# Equivalent to (i + j) mod 2
# (I'm just evil)
if ( ( i + j ) and 1 ) = = 0 :
2023-11-13 09:52:37 +01:00
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 :
2023-11-13 09:52:37 +01:00
result & = " \x1b [0m "
2023-10-15 22:46:22 +02:00
else :
2023-11-13 09:52:37 +01:00
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 "
2023-10-15 16:53:44 +02:00
2023-10-28 02:32:50 +02:00
proc toFEN * ( self : ChessBoard ) : string =
## Returns a FEN string of the current
## position in the chessboard
var skip : int
# Piece placement data
2023-11-13 09:52:37 +01:00
for i in 0 .. 7 :
2023-10-28 02:32:50 +02:00
skip = 0
2023-11-13 09:52:37 +01:00
for j in 0 .. 7 :
let piece = self . grid [ i , j ]
2023-10-28 02:32:50 +02:00
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 ( )
2023-10-30 14:46:27 +01:00
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
2023-10-31 23:06:27 +01:00
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
if ply = = 0 :
result . nodes = 1
return
elif ply = = 1 and bulk :
2023-10-31 23:06:27 +01:00
if divide :
var postfix = " "
for move in moves :
2023-11-13 09:52:37 +01:00
case move . getPromotionType ( ) :
2023-10-31 23:06:27 +01:00
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 " "
2023-10-30 14:46:27 +01:00
return ( uint64 ( len ( moves ) ) , 0 , 0 , 0 , 0 , 0 , 0 )
2023-10-28 02:32:50 +02:00
for move in moves :
if verbose :
let canCastle = self . canCastle ( self . getActiveColor ( ) )
2023-10-31 23:06:27 +01:00
echo & " Ply (from root): {self.position.plyFromRoot} "
2023-10-28 02:32:50 +02:00
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()} "
2023-10-30 14:46:27 +01:00
echo & " In check: {(if self.inCheck(): \" yes \" else: \" no \" )} "
2023-10-28 02:32:50 +02:00
echo & " Can castle: \n - King side: {(if canCastle.king: \" yes \" else: \" no \" )} \n - Queen side: {(if canCastle.queen: \" yes \" else: \" no \" )} "
2023-10-30 14:46:27 +01:00
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 ( )
2023-10-28 02:32:50 +02:00
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 )
2023-10-30 14:46:27 +01:00
if self . inCheck ( ) :
# Opponent king is in check
inc ( result . checks )
2023-10-28 02:32:50 +02:00
if verbose :
2023-10-30 14:46:27 +01:00
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 ( )
2023-10-31 23:06:27 +01:00
stdout . write ( " nextpos>> " )
2023-10-28 02:32:50 +02:00
try :
discard readLine ( stdin )
except IOError :
discard
except EOFError :
discard
2023-10-30 14:46:27 +01:00
let next = self . perft ( ply - 1 , verbose , bulk = bulk )
2023-10-31 23:06:27 +01:00
self . undoLastMove ( )
if divide and ( not bulk or ply > 1 ) :
2023-10-30 14:46:27 +01:00
var postfix = " "
2023-11-13 09:52:37 +01:00
if move . isPromotion ( ) :
case move . getPromotionType ( ) :
of PromoteToBishop :
postfix = " b "
of PromoteToKnight :
postfix = " n "
of PromoteToRook :
postfix = " r "
of PromoteToQueen :
postfix = " q "
else :
discard
2023-10-30 14:46:27 +01:00
echo & " {move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()}{postfix}: {next.nodes} "
2023-10-28 02:32:50 +02:00
if verbose :
echo " "
2023-10-30 14:46:27 +01:00
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
2023-10-28 02:32:50 +02:00
2023-11-13 09:52:37 +01:00
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 " :
2023-11-13 09:52:37 +01:00
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 & " \n Nodes searched (bulk-counting: on): {nodes} "
echo & " Time taken: {round(cpuTime() - t, 3)} seconds \n "
2023-11-13 09:52:37 +01:00
else :
2024-04-08 20:28:31 +02:00
let t = cpuTime ( )
2023-11-13 09:52:37 +01:00
let data = board . perft ( ply , divide = true , verbose = verbose )
echo & " \n Nodes 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 "
2023-11-13 09:52:37 +01:00
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 . } =
2023-11-13 09:52:37 +01:00
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
2023-11-13 09:52:37 +01:00
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]}) "
2023-11-13 09:52:37 +01:00
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]}) "
2023-11-13 09:52:37 +01:00
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.)
2023-11-13 09:52:37 +01:00
if board . grid [ targetSquare . row , targetSquare . col ] . kind ! = Empty :
2023-11-13 11:03:54 +01:00
flags = flags or Capture . uint16
2023-11-13 09:52:37 +01:00
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
2023-11-13 09:52:37 +01:00
if len ( moveString ) = = 5 :
# Promotion
case moveString [ 4 ] :
of ' b ' :
2023-11-13 11:03:54 +01:00
flags = flags or PromoteToBishop . uint16
2023-11-13 09:52:37 +01:00
of ' n ' :
2023-11-13 11:03:54 +01:00
flags = flags or PromoteToKnight . uint16
2023-11-13 09:52:37 +01:00
of ' q ' :
2023-11-13 11:03:54 +01:00
flags = flags or PromoteToQueen . uint16
2023-11-13 09:52:37 +01:00
of ' r ' :
2023-11-13 11:03:54 +01:00
flags = flags or PromoteToRook . uint16
2023-11-13 09:52:37 +01:00
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
result = board . makeMove ( move )
if result = = emptyMove ( ) :
echo & " Error: move: {moveString} is illegal "
proc handlePositionCommand ( board : var ChessBoard , command : seq [ string ] ) =
if len ( command ) < 2 :
echo " Error: position: invalid number of arguments "
return
# Makes sure we don't leave the board in an invalid state if
# some error occurs
var tempBoard = newChessboard ( )
case command [ 1 ] :
of " startpos " :
tempBoard = newDefaultChessboard ( )
if command . len ( ) > 2 :
let args = command [ 2 ] . splitWhitespace ( )
if args . len ( ) > 0 :
var i = 0
while i < args . len ( ) :
case args [ i ] :
of " moves " :
var j = i + 1
while j < args . len ( ) :
if handleMoveCommand ( tempBoard , @ [ " move " , args [ j ] ] ) = = emptyMove ( ) :
return
inc ( j )
inc ( i )
board = tempBoard
of " fen " :
if len ( command ) = = 2 :
echo & " Current position: {board.toFEN()} "
return
var
args = command [ 2 ] . splitWhitespace ( )
fenString = " "
stop = 0
for i , arg in args :
if arg in [ " moves " , ] :
break
if i > 0 :
fenString & = " "
fenString & = arg
inc ( stop )
args = args [ stop .. ^ 1 ]
try :
tempBoard = newChessboardFromFEN ( fenString )
except ValueError :
echo & " Error: position: {getCurrentExceptionMsg()} "
return
if args . len ( ) > 0 :
var i = 0
while i < args . len ( ) :
case args [ i ] :
of " moves " :
var j = i + 1
while j < args . len ( ) :
if handleMoveCommand ( tempBoard , @ [ " move " , args [ j ] ] ) = = emptyMove ( ) :
return
inc ( j )
inc ( i )
board = tempBoard
of " print " :
echo board
of " pretty " :
echo board . pretty ( )
const HELP_TEXT = """ Nimfish help menu:
- go : Begin a search
Subcommands :
- perft < depth > [ options ] : Run the performance test at the given depth ( in ply ) and
print the results
Options :
- bulk : Enable bulk - counting ( significantly faster , gives less statistics )
- verbose : Enable move debugging ( for each and every move , not recommended on large searches )
Example : go perft 5 bulk
- position : Get / set board position
Subcommands :
- fen [ string ] : Set the board to the given fen string if one is provided , or print
the current position as a FEN string if no arguments are given
- startpos : Set the board to the starting position
- pretty : Pretty - print the current position
- print : Print the current position using ASCII characters only
Options :
- moves { moveList } : Perform the given moves ( space - separated , all - lowercase )
in algebraic notation after the position is loaded . This option only applies
to the " startpos " and " fen " subcommands : it is ignored otherwise
Examples :
- position startpos
- position fen " ... " moves a2a3 a7a6
- clear : Clear the screen
- move < move > : Perform the given move in algebraic notation
- castle : Print castling rights for each side
- check : Print if the current side to move is in check
- undo : Undoes the last move that was performed . Can be used in succession
- turn : Print which side is to move
- ep : Print the current en passant target
- pretty : Shorthand for " position pretty "
- print : Shorthand for " position print "
2024-04-09 17:46:30 +02:00
- get < square > : Get the piece on the given square
2024-04-08 20:28:31 +02:00
"""
2023-11-13 09:52:37 +01:00
2023-10-30 14:46:27 +01:00
proc main : int =
2023-10-31 23:06:27 +01:00
## Nimfish's control interface
2023-10-30 14:46:27 +01:00
echo " Nimfish by nocturn9x (see LICENSE) "
var board = newDefaultChessboard ( )
while true :
2023-10-31 23:06:27 +01:00
var
cmd : seq [ string ]
cmdStr : string
2023-10-30 14:46:27 +01:00
try :
stdout . write ( " >>> " )
stdout . flushFile ( )
2023-10-31 23:06:27 +01:00
cmdStr = readLine ( stdin ) . strip ( leading = true , trailing = true , chars = { ' \t ' , ' ' } )
if cmdStr . len ( ) = = 0 :
continue
cmd = cmdStr . splitWhitespace ( maxsplit = 2 )
2023-10-30 14:46:27 +01:00
case cmd [ 0 ] :
of " clear " :
echo " \x1B c "
of " help " :
2024-04-08 20:28:31 +02:00
echo HELP_TEXT
2023-10-30 14:46:27 +01:00
of " go " :
2023-11-13 09:52:37 +01:00
handleGoCommand ( board , cmd )
2023-10-30 14:46:27 +01:00
of " position " :
2023-11-13 09:52:37 +01:00
handlePositionCommand ( board , cmd )
of " move " :
handleMoveCommand ( board , cmd )
2024-04-08 20:28:31 +02:00
of " pretty " , " print " :
handlePositionCommand ( board , @ [ " position " , cmd [ 0 ] ] )
2023-11-13 09:52:37 +01:00
of " undo " :
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 "
2024-04-09 17:46:30 +02:00
of " get " :
if len ( cmd ) ! = 2 :
echo " get: invalid syntax "
continue
try :
echo board . getPiece ( cmd [ 1 ] )
except ValueError :
echo " 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 :
2023-10-30 14:46:27 +01:00
echo & " Unknown command ' {cmd[0]} ' . Type ' help ' for more information. "
except IOError :
echo " "
return - 1
except EOFError :
echo " "
return 0
2023-10-21 18:19:41 +02:00
2023-10-13 11:54:57 +02:00
when isMainModule :
proc testPiece ( piece : Piece , kind : PieceKind , color : PieceColor ) =
doAssert piece . kind = = kind and piece . color = = color , & " expected piece of kind {kind} and color {color}, got {piece.kind} / {piece.color} instead "
2023-10-13 12:26:14 +02:00
proc testPieceCount ( board : ChessBoard , kind : PieceKind , color : PieceColor , count : int ) =
let pieces = board . countPieces ( kind , color )
doAssert pieces = = count , & " expected {count} pieces of kind {kind} and color {color}, got {pieces} instead "
2023-10-13 11:54:57 +02:00
var b = newDefaultChessboard ( )
2023-10-13 12:26:14 +02:00
# Ensure correct number of pieces
testPieceCount ( b , Pawn , White , 8 )
testPieceCount ( b , Pawn , Black , 8 )
testPieceCount ( b , Knight , White , 2 )
testPieceCount ( b , Knight , Black , 2 )
testPieceCount ( b , Bishop , White , 2 )
testPieceCount ( b , Bishop , Black , 2 )
testPieceCount ( b , Rook , White , 2 )
testPieceCount ( b , Rook , Black , 2 )
testPieceCount ( b , Queen , White , 1 )
testPieceCount ( b , Queen , Black , 1 )
testPieceCount ( b , King , White , 1 )
testPieceCount ( b , King , Black , 1 )
# Ensure pieces are in the correct location
# Pawns
2023-10-13 11:54:57 +02:00
for loc in [ " a2 " , " b2 " , " c2 " , " d2 " , " e2 " , " f2 " , " g2 " , " h2 " ] :
testPiece ( b . getPiece ( loc ) , Pawn , White )
for loc in [ " a7 " , " b7 " , " c7 " , " d7 " , " e7 " , " f7 " , " g7 " , " h7 " ] :
testPiece ( b . getPiece ( loc ) , Pawn , Black )
# Rooks
testPiece ( b . getPiece ( " a1 " ) , Rook , White )
testPiece ( b . getPiece ( " h1 " ) , Rook , White )
testPiece ( b . getPiece ( " a8 " ) , Rook , Black )
testPiece ( b . getPiece ( " h8 " ) , Rook , Black )
# Knights
testPiece ( b . getPiece ( " b1 " ) , Knight , White )
testPiece ( b . getPiece ( " g1 " ) , Knight , White )
testPiece ( b . getPiece ( " b8 " ) , Knight , Black )
testPiece ( b . getPiece ( " g8 " ) , Knight , Black )
# Bishops
testPiece ( b . getPiece ( " c1 " ) , Bishop , White )
testPiece ( b . getPiece ( " f1 " ) , Bishop , White )
testPiece ( b . getPiece ( " c8 " ) , Bishop , Black )
testPiece ( b . getPiece ( " f8 " ) , Bishop , Black )
# Kings
testPiece ( b . getPiece ( " e1 " ) , King , White )
testPiece ( b . getPiece ( " e8 " ) , King , Black )
# Queens
testPiece ( b . getPiece ( " d1 " ) , Queen , White )
testPiece ( b . getPiece ( " d8 " ) , Queen , Black )
2023-10-31 23:06:27 +01:00
setControlCHook ( proc ( ) {. noconv . } = quit ( 0 ) )
2023-10-30 14:46:27 +01:00
quit ( main ( ) )