More bug fixes. Still borked. Improve modularity
This commit is contained in:
parent
0496047164
commit
64c30b8a90
|
@ -50,8 +50,8 @@ type
|
||||||
# En passant target square (see https://en.wikipedia.org/wiki/En_passant)
|
# En passant target square (see https://en.wikipedia.org/wiki/En_passant)
|
||||||
enPassantSquare*: Square
|
enPassantSquare*: Square
|
||||||
|
|
||||||
# Active color
|
# The side to move
|
||||||
turn: PieceColor
|
sideToMove: PieceColor
|
||||||
# Positional bitboards for all pieces
|
# Positional bitboards for all pieces
|
||||||
pieces: tuple[white, black: tuple[king, queens, rooks, bishops, knights, pawns: Bitboard]]
|
pieces: tuple[white, black: tuple[king, queens, rooks, bishops, knights, pawns: Bitboard]]
|
||||||
# Pinned pieces for each side
|
# Pinned pieces for each side
|
||||||
|
@ -69,14 +69,12 @@ type
|
||||||
position: Position
|
position: Position
|
||||||
# List of all previously reached positions
|
# List of all previously reached positions
|
||||||
positions: seq[Position]
|
positions: seq[Position]
|
||||||
# Index of the current position
|
|
||||||
currPos: int
|
|
||||||
|
|
||||||
|
|
||||||
# A bunch of simple utility functions and forward declarations
|
# A bunch of simple utility functions and forward declarations
|
||||||
proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.}
|
proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.}
|
||||||
proc isLegal(self: ChessBoard, move: Move): bool {.inline.}
|
proc isLegal(self: ChessBoard, move: Move): bool {.inline.}
|
||||||
proc doMove(self: ChessBoard, move: Move)
|
proc doMove*(self: ChessBoard, move: Move)
|
||||||
proc pretty*(self: ChessBoard): string
|
proc pretty*(self: ChessBoard): string
|
||||||
proc spawnPiece(self: ChessBoard, square: Square, piece: Piece)
|
proc spawnPiece(self: ChessBoard, square: Square, piece: Piece)
|
||||||
proc toFEN*(self: ChessBoard): string
|
proc toFEN*(self: ChessBoard): string
|
||||||
|
@ -89,13 +87,16 @@ proc extend[T](self: var seq[T], other: openarray[T]) {.inline.} =
|
||||||
for x in other:
|
for x in other:
|
||||||
self.add(x)
|
self.add(x)
|
||||||
|
|
||||||
proc updateBoard*(self: ChessBoard)
|
proc update*(self: ChessBoard)
|
||||||
|
|
||||||
|
|
||||||
|
func setSideToMove*(self: ChessBoard, side: PieceColor) {.inline.} =
|
||||||
|
self.position.sideToMove = side
|
||||||
|
|
||||||
# A bunch of getters
|
# A bunch of getters
|
||||||
func getSideToMove*(self: ChessBoard): PieceColor {.inline.} =
|
func getSideToMove*(self: ChessBoard): PieceColor {.inline.} =
|
||||||
## Returns the currently side to move
|
## Returns the currently side to move
|
||||||
return self.position.turn
|
return self.position.sideToMove
|
||||||
|
|
||||||
|
|
||||||
func getEnPassantTarget*(self: ChessBoard): Square {.inline.} =
|
func getEnPassantTarget*(self: ChessBoard): Square {.inline.} =
|
||||||
|
@ -103,6 +104,11 @@ func getEnPassantTarget*(self: ChessBoard): Square {.inline.} =
|
||||||
return self.position.enPassantSquare
|
return self.position.enPassantSquare
|
||||||
|
|
||||||
|
|
||||||
|
func getPlyFromRoot*(self: ChessBoard): int8 {.inline.} =
|
||||||
|
## Returns the current distance from the root in plys
|
||||||
|
return self.position.plyFromRoot
|
||||||
|
|
||||||
|
|
||||||
func getMoveCount*(self: ChessBoard): int {.inline.} =
|
func getMoveCount*(self: ChessBoard): int {.inline.} =
|
||||||
## Returns the number of full moves that
|
## Returns the number of full moves that
|
||||||
## have been played
|
## have been played
|
||||||
|
@ -136,7 +142,7 @@ func getStartRank(piece: Piece): int {.inline.} =
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
func getKingStartingSquare(color: PieceColor): Square {.inline.} =
|
func getKingStartingSquare*(color: PieceColor): Square {.inline.} =
|
||||||
## Retrieves the starting square of the king
|
## Retrieves the starting square of the king
|
||||||
## for the given color
|
## for the given color
|
||||||
case color:
|
case color:
|
||||||
|
@ -148,14 +154,16 @@ func getKingStartingSquare(color: PieceColor): Square {.inline.} =
|
||||||
discard
|
discard
|
||||||
|
|
||||||
|
|
||||||
func kingSideRook(color: PieceColor): Square {.inline.} = (if color == White: "h1".toSquare() else: "h8".toSquare())
|
# FIXME: Check this shit.
|
||||||
func queenSideRook(color: PieceColor): Square {.inline.} = (if color == White: "a8".toSquare() else: "a1".toSquare())
|
func kingSideRook*(color: PieceColor): Square {.inline.} = (if color == White: "h1".toSquare() else: "h8".toSquare())
|
||||||
func longCastleKing(color: PieceColor): Square {.inline.} = (if color == White: "c1".toSquare() else: "c8".toSquare())
|
func queenSideRook*(color: PieceColor): Square {.inline.} = (if color == White: "a8".toSquare() else: "a1".toSquare())
|
||||||
func shortCastleKing(color: PieceColor): Square {.inline.} = (if color == White: "g1".toSquare() else: "g8".toSquare())
|
func longCastleKing*(color: PieceColor): Square {.inline.} = (if color == White: "c1".toSquare() else: "c8".toSquare())
|
||||||
func longCastleRook(color: PieceColor): Square {.inline.} = (if color == White: makeSquare(7, 3) else: makeSquare(7, 5))
|
func shortCastleKing*(color: PieceColor): Square {.inline.} = (if color == White: "g1".toSquare() else: "g8".toSquare())
|
||||||
func shortCastleRook(color: PieceColor): Square {.inline.} = (if color == White: makeSquare(0, 0) else: makeSquare(0, 2))
|
func longCastleRook*(color: PieceColor): Square {.inline.} = (if color == White: "d1".toSquare() else: "d8".toSquare())
|
||||||
|
func shortCastleRook*(color: PieceColor): Square {.inline.} = (if color == White: "f1".toSquare() else: "f8".toSquare())
|
||||||
|
|
||||||
proc inCheck(self: ChessBoard, side: PieceColor): bool
|
|
||||||
|
proc inCheck*(self: ChessBoard, side: PieceColor): bool
|
||||||
|
|
||||||
|
|
||||||
proc newChessboard: ChessBoard =
|
proc newChessboard: ChessBoard =
|
||||||
|
@ -163,8 +171,7 @@ proc newChessboard: ChessBoard =
|
||||||
new(result)
|
new(result)
|
||||||
for i in 0..63:
|
for i in 0..63:
|
||||||
result.grid[i] = nullPiece()
|
result.grid[i] = nullPiece()
|
||||||
result.currPos = -1
|
result.position = Position(enPassantSquare: nullSquare(), sideToMove: White)
|
||||||
result.position = Position(enPassantSquare: nullSquare(), turn: White)
|
|
||||||
|
|
||||||
# Indexing operations
|
# Indexing operations
|
||||||
func `[]`(self: array[64, Piece], square: Square): Piece {.inline.} = self[square.int8]
|
func `[]`(self: array[64, Piece], square: Square): Piece {.inline.} = self[square.int8]
|
||||||
|
@ -314,9 +321,9 @@ proc newChessboardFromFEN*(fen: string): ChessBoard =
|
||||||
# Active color
|
# Active color
|
||||||
case c:
|
case c:
|
||||||
of 'w':
|
of 'w':
|
||||||
result.position.turn = White
|
result.position.sideToMove = White
|
||||||
of 'b':
|
of 'b':
|
||||||
result.position.turn = Black
|
result.position.sideToMove = Black
|
||||||
else:
|
else:
|
||||||
raise newException(ValueError, &"invalid FEN: invalid active color identifier '{c}'")
|
raise newException(ValueError, &"invalid FEN: invalid active color identifier '{c}'")
|
||||||
of 2:
|
of 2:
|
||||||
|
@ -504,12 +511,9 @@ func getFlags*(move: Move): seq[MoveFlag] =
|
||||||
result.add(Default)
|
result.add(Default)
|
||||||
|
|
||||||
|
|
||||||
func getKingSquare(self: ChessBoard, color: PieceColor = None): Square {.inline.} =
|
func getKingSquare*(self: ChessBoard, color: PieceColor): Square {.inline.} =
|
||||||
## Returns the square of the king for the given
|
## Returns the square of the king for the given
|
||||||
## side (if it is None, the side to move is used)
|
## side
|
||||||
var color = color
|
|
||||||
if color == None:
|
|
||||||
color = self.getSideToMove()
|
|
||||||
case color:
|
case color:
|
||||||
of White:
|
of White:
|
||||||
return self.position.pieces.white.king.toSquare()
|
return self.position.pieces.white.king.toSquare()
|
||||||
|
@ -539,7 +543,6 @@ proc getOccupancy(self: ChessBoard): Bitboard =
|
||||||
result = self.getOccupancyFor(Black) or self.getOccupancyFor(White)
|
result = self.getOccupancyFor(Black) or self.getOccupancyFor(White)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
proc getPawnAttacks(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard =
|
proc getPawnAttacks(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard =
|
||||||
## Returns the attack bitboard for the given square from
|
## Returns the attack bitboard for the given square from
|
||||||
## the pawns of the given side
|
## the pawns of the given side
|
||||||
|
@ -601,7 +604,7 @@ proc getSlidingAttacks(self: ChessBoard, square: Square, attacker: PieceColor):
|
||||||
result = result or queen.toBitboard()
|
result = result or queen.toBitboard()
|
||||||
|
|
||||||
|
|
||||||
proc getAttacksTo(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard =
|
proc getAttacksTo*(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard =
|
||||||
## Computes the attack bitboard for the given square from
|
## Computes the attack bitboard for the given square from
|
||||||
## the given side
|
## the given side
|
||||||
result = Bitboard(0)
|
result = Bitboard(0)
|
||||||
|
@ -613,12 +616,30 @@ proc getAttacksTo(self: ChessBoard, square: Square, attacker: PieceColor): Bitbo
|
||||||
result = result or self.getSlidingAttacks(square, attacker)
|
result = result or self.getSlidingAttacks(square, attacker)
|
||||||
|
|
||||||
|
|
||||||
|
proc updateCheckers(self: ChessBoard) =
|
||||||
|
let side = self.getSideToMove()
|
||||||
|
let checkers = self.getAttacksTo(self.getKingSquare(side), side.opposite())
|
||||||
|
case side:
|
||||||
|
of White:
|
||||||
|
self.position.checkers.white = checkers
|
||||||
|
of Black:
|
||||||
|
self.position.checkers.black = checkers
|
||||||
|
else:
|
||||||
|
discard
|
||||||
|
|
||||||
|
|
||||||
proc inCheck(self: ChessBoard, side: PieceColor): bool =
|
proc inCheck(self: ChessBoard, side: PieceColor): bool =
|
||||||
## Returns if the current side to move is in check
|
## Returns if the current side to move is in check
|
||||||
return self.getAttacksTo(self.getKingSquare(side), side.opposite()) != 0
|
case self.getSideToMove():
|
||||||
|
of White:
|
||||||
|
return self.position.checkers.white != 0
|
||||||
|
of Black:
|
||||||
|
return self.position.checkers.black != 0
|
||||||
|
else:
|
||||||
|
discard
|
||||||
|
|
||||||
|
|
||||||
proc canCastle(self: ChessBoard, side: PieceColor): tuple[king, queen: bool] =
|
proc canCastle*(self: ChessBoard, side: PieceColor): tuple[king, queen: bool] =
|
||||||
## Returns if the current side to move can castle
|
## Returns if the current side to move can castle
|
||||||
return (false, false) # TODO
|
return (false, false) # TODO
|
||||||
|
|
||||||
|
@ -659,10 +680,12 @@ proc generatePawnCaptures(self: ChessBoard, moves: var MoveList) =
|
||||||
pawns = self.getBitboard(Pawn, sideToMove)
|
pawns = self.getBitboard(Pawn, sideToMove)
|
||||||
# We can only capture enemy pieces (except the king)
|
# We can only capture enemy pieces (except the king)
|
||||||
enemyPieces = self.getCapturablePieces(nonSideToMove)
|
enemyPieces = self.getCapturablePieces(nonSideToMove)
|
||||||
|
enemyPawns = self.getBitboard(Pawn, nonSideToMove)
|
||||||
rightMovement = pawns.forwardRightRelativeTo(sideToMove)
|
rightMovement = pawns.forwardRightRelativeTo(sideToMove)
|
||||||
leftMovement = pawns.forwardLeftRelativeTo(sideToMove)
|
leftMovement = pawns.forwardLeftRelativeTo(sideToMove)
|
||||||
epTarget = self.getEnPassantTarget()
|
epTarget = self.getEnPassantTarget()
|
||||||
let epBitboard = if (epTarget != nullSquare()): epTarget.toBitboard() else: Bitboard(0)
|
var epBitboard = if (epTarget != nullSquare()): epTarget.toBitboard() else: Bitboard(0)
|
||||||
|
epBitboard = epBitboard and enemyPawns
|
||||||
# Top right attacks
|
# Top right attacks
|
||||||
for square in rightMovement and enemyPieces:
|
for square in rightMovement and enemyPieces:
|
||||||
moves.add(createMove(square.toBitboard().backwardLeftRelativeTo(sideToMove), square, Capture))
|
moves.add(createMove(square.toBitboard().backwardLeftRelativeTo(sideToMove), square, Capture))
|
||||||
|
@ -704,13 +727,11 @@ proc generateRookMovements(self: ChessBoard, moves: var MoveList) =
|
||||||
let
|
let
|
||||||
sideToMove = self.getSideToMove()
|
sideToMove = self.getSideToMove()
|
||||||
occupancy = self.getOccupancy()
|
occupancy = self.getOccupancy()
|
||||||
friendlyPieces = self.getOccupancyFor(sideToMove)
|
|
||||||
rooks = self.getBitboard(Rook, sideToMove)
|
rooks = self.getBitboard(Rook, sideToMove)
|
||||||
for square in rooks:
|
for square in rooks:
|
||||||
let blockers = occupancy and Rook.getRelevantBlockers(square)
|
let blockers = occupancy and Rook.getRelevantBlockers(square)
|
||||||
var moveset = getRookMoves(square, blockers)
|
# Can't move over other pieces (captures are handled elsewhere)
|
||||||
# Can't move over our own pieces
|
let moveset = getRookMoves(square, blockers) and not occupancy
|
||||||
moveset = moveset and not friendlyPieces
|
|
||||||
for target in moveset:
|
for target in moveset:
|
||||||
moves.add(createMove(square, target))
|
moves.add(createMove(square, target))
|
||||||
|
|
||||||
|
@ -744,13 +765,12 @@ proc generateBishopMovements(self: ChessBoard, moves: var MoveList) =
|
||||||
let
|
let
|
||||||
sideToMove = self.getSideToMove()
|
sideToMove = self.getSideToMove()
|
||||||
occupancy = self.getOccupancy()
|
occupancy = self.getOccupancy()
|
||||||
friendlyPieces = self.getOccupancyFor(sideToMove)
|
|
||||||
bishops = self.getBitboard(Bishop, sideToMove)
|
bishops = self.getBitboard(Bishop, sideToMove)
|
||||||
for square in bishops:
|
for square in bishops:
|
||||||
let blockers = occupancy and Bishop.getRelevantBlockers(square)
|
let blockers = occupancy and Bishop.getRelevantBlockers(square)
|
||||||
var moveset = getBishopMoves(square, blockers)
|
var moveset = getBishopMoves(square, blockers)
|
||||||
# Can't move over our own pieces
|
# Can't move over other pieces (captures are handled elsewhere)
|
||||||
moveset = moveset and not friendlyPieces
|
moveset = moveset and not occupancy
|
||||||
for target in moveset:
|
for target in moveset:
|
||||||
moves.add(createMove(square, target))
|
moves.add(createMove(square, target))
|
||||||
|
|
||||||
|
@ -795,8 +815,8 @@ proc generateQueenMovements(self: ChessBoard, moves: var MoveList) =
|
||||||
rookMoves = getRookMoves(square, rookBlockers)
|
rookMoves = getRookMoves(square, rookBlockers)
|
||||||
bishopMoves = getBishopMoves(square, bishopBlockers)
|
bishopMoves = getBishopMoves(square, bishopBlockers)
|
||||||
var moveset = rookMoves or bishopMoves
|
var moveset = rookMoves or bishopMoves
|
||||||
# Can't move over our own pieces
|
# Can't move over other pieces (captures are handled elsewhere)
|
||||||
moveset = moveset and not friendlyPieces
|
moveset = moveset and not occupancy
|
||||||
for target in moveset:
|
for target in moveset:
|
||||||
moves.add(createMove(square, target))
|
moves.add(createMove(square, target))
|
||||||
|
|
||||||
|
@ -871,77 +891,17 @@ proc generateKnightMoves(self: ChessBoard, moves: var MoveList)=
|
||||||
moves.add(createMove(square, target, Capture))
|
moves.add(createMove(square, target, Capture))
|
||||||
|
|
||||||
|
|
||||||
proc checkInsufficientMaterialPieceCount(self: ChessBoard, color: PieceColor): bool =
|
|
||||||
## Helper function for checkInsufficientMaterial
|
|
||||||
let
|
|
||||||
friendlyPawns = self.countPieces(Piece(kind: Pawn, color: color))
|
|
||||||
friendlyRooks = self.countPieces(Piece(kind: Rook, color: color))
|
|
||||||
friendlyQueens = self.countPieces(Piece(kind: Queen, color: color))
|
|
||||||
friendlyKnights = self.countPieces(Piece(kind: Knight, color: color))
|
|
||||||
friendlyBishops = self.countPieces(Piece(kind: Bishop, color: color))
|
|
||||||
enemyPawns = self.countPieces(Piece(kind: Pawn, color: color.opposite()))
|
|
||||||
enemyRooks = self.countPieces(Piece(kind: Rook, color: color.opposite()))
|
|
||||||
enemyQueens = self.countPieces(Piece(kind: Queen, color: color.opposite()))
|
|
||||||
enemyKnights = self.countPieces(Piece(kind: Knight, color: color.opposite()))
|
|
||||||
enemyBishops = self.countPieces(Piece(kind: Bishop, color: color.opposite()))
|
|
||||||
if friendlyPawns > 0 or friendlyRooks > 0 or friendlyQueens > 0:
|
|
||||||
return false
|
|
||||||
if friendlyKnights >= 2:
|
|
||||||
return false
|
|
||||||
if friendlyKnights + friendlyBishops >= 2:
|
|
||||||
return false
|
|
||||||
if friendlyKnights >= 1 and (enemyPawns > 0 or enemyRooks > 0 or enemyBishops > 0 or enemyKnights > 0 or enemyQueens > 0):
|
|
||||||
return false
|
|
||||||
if friendlyBishops >= 1 and (enemyKnights > 0 or enemyPawns > 0):
|
|
||||||
return false
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
proc checkInsufficientMaterial(self: ChessBoard): bool =
|
|
||||||
## Checks if the given position has not enough material for either side to
|
|
||||||
## checkmate the enemy king. Note that the criteria as implemented here are
|
|
||||||
## not fully compliant with FIDE rules (they just define a draw by insufficient
|
|
||||||
## material as "[...] the position is such that the opponent cannot checkmate
|
|
||||||
## the player’s king by any possible series of legal moves.", which is really
|
|
||||||
## tricky to implement efficiently). For more info see https://www.reddit.com/r/chess/comments/se89db/a_writeup_on_definitions_of_insufficient_material/
|
|
||||||
if not (self.checkInsufficientMaterialPieceCount(White) and self.checkInsufficientMaterialPieceCount(Black)):
|
|
||||||
return false
|
|
||||||
let
|
|
||||||
whiteBishops = self.countPieces(Piece(kind: Bishop, color: White))
|
|
||||||
blackBishops = self.countPieces(Piece(kind: Bishop, color: Black))
|
|
||||||
if blackBishops + whiteBishops >= 2:
|
|
||||||
var
|
|
||||||
darkSquare = 0
|
|
||||||
lightSquare = 0
|
|
||||||
for bishop in self.position.pieces.black.bishops:
|
|
||||||
if bishop.isLightSquare():
|
|
||||||
lightSquare += 1
|
|
||||||
else:
|
|
||||||
darkSquare += 1
|
|
||||||
for bishop in self.position.pieces.white.bishops:
|
|
||||||
if bishop.isLightSquare():
|
|
||||||
lightSquare += 1
|
|
||||||
else:
|
|
||||||
darkSquare += 1
|
|
||||||
if darkSquare >= 1 and lightSquare >= 1:
|
|
||||||
return false
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
proc generateMoves*(self: ChessBoard, moves: var MoveList) =
|
proc generateMoves*(self: ChessBoard, moves: var MoveList) =
|
||||||
## Generates the list of all possible legal moves
|
## Generates the list of all possible legal moves
|
||||||
## in the current position
|
## in the current position
|
||||||
|
moves.clear()
|
||||||
if self.position.halfMoveClock >= 100:
|
if self.position.halfMoveClock >= 100:
|
||||||
# Draw by 50-move rule
|
# Draw by 50-move rule
|
||||||
return
|
return
|
||||||
# TODO: Check for draw by insufficient material
|
# TODO: Check for draw by insufficient material
|
||||||
#[
|
|
||||||
if self.checkInsufficientMaterial():
|
|
||||||
return @[]
|
|
||||||
]#
|
|
||||||
# TODO: Check for repetitions (requires zobrist hashing + table)
|
# TODO: Check for repetitions (requires zobrist hashing + table)
|
||||||
self.generatePawnMoves(moves)
|
|
||||||
self.generateKingMoves(moves)
|
self.generateKingMoves(moves)
|
||||||
|
self.generatePawnMoves(moves)
|
||||||
self.generateKnightMoves(moves)
|
self.generateKnightMoves(moves)
|
||||||
self.generateSlidingMoves(moves)
|
self.generateSlidingMoves(moves)
|
||||||
|
|
||||||
|
@ -1031,7 +991,8 @@ proc removePiece(self: ChessBoard, square: Square) =
|
||||||
## Removes a piece from the board, updating necessary
|
## Removes a piece from the board, updating necessary
|
||||||
## metadata
|
## metadata
|
||||||
var piece = self.grid[square]
|
var piece = self.grid[square]
|
||||||
doAssert piece.kind != Empty and piece.color != None
|
when not defined(danger):
|
||||||
|
doAssert piece.kind != Empty and piece.color != None, self.toFEN()
|
||||||
self.removePieceFromBitboard(square)
|
self.removePieceFromBitboard(square)
|
||||||
self.grid[square] = nullPiece()
|
self.grid[square] = nullPiece()
|
||||||
|
|
||||||
|
@ -1042,9 +1003,10 @@ proc movePiece(self: ChessBoard, move: Move) =
|
||||||
## not update attacked squares metadata, just
|
## not update attacked squares metadata, just
|
||||||
## positional info and the grid itself
|
## positional info and the grid itself
|
||||||
let piece = self.grid[move.startSquare]
|
let piece = self.grid[move.startSquare]
|
||||||
let targetSquare = self.getPiece(move.targetSquare)
|
when not defined(danger):
|
||||||
if targetSquare.color != None:
|
let targetSquare = self.getPiece(move.targetSquare)
|
||||||
raise newException(AccessViolationDefect, &"{piece} at {move.startSquare} attempted to overwrite {targetSquare} at {move.targetSquare}")
|
if targetSquare.color != None:
|
||||||
|
raise newException(AccessViolationDefect, &"{piece} at {move.startSquare} attempted to overwrite {targetSquare} at {move.targetSquare}: {move}")
|
||||||
# Update positional metadata
|
# Update positional metadata
|
||||||
self.removePiece(move.startSquare)
|
self.removePiece(move.startSquare)
|
||||||
self.spawnPiece(move.targetSquare, piece)
|
self.spawnPiece(move.targetSquare, piece)
|
||||||
|
@ -1055,7 +1017,7 @@ proc movePiece(self: ChessBoard, startSquare, targetSquare: Square) =
|
||||||
self.movePiece(Move(startSquare: startSquare, targetSquare: targetSquare))
|
self.movePiece(Move(startSquare: startSquare, targetSquare: targetSquare))
|
||||||
|
|
||||||
|
|
||||||
proc doMove(self: ChessBoard, move: Move) =
|
proc doMove*(self: ChessBoard, move: Move) =
|
||||||
## Internal function called by makeMove after
|
## Internal function called by makeMove after
|
||||||
## performing legality checks. Can be used in
|
## performing legality checks. Can be used in
|
||||||
## performance-critical paths where a move is
|
## performance-critical paths where a move is
|
||||||
|
@ -1063,11 +1025,11 @@ proc doMove(self: ChessBoard, move: Move) =
|
||||||
|
|
||||||
# Record final position for future reference
|
# Record final position for future reference
|
||||||
self.positions.add(self.position)
|
self.positions.add(self.position)
|
||||||
inc(self.currPos)
|
|
||||||
|
|
||||||
# Final checks
|
# Final checks
|
||||||
let piece = self.grid[move.startSquare]
|
let piece = self.grid[move.startSquare]
|
||||||
doAssert piece.kind != Empty and piece.color != None
|
when not defined(danger):
|
||||||
|
doAssert piece.kind != Empty and piece.color != None, &"{move} {self.toFEN()}"
|
||||||
|
|
||||||
var
|
var
|
||||||
halfMoveClock = self.position.halfMoveClock
|
halfMoveClock = self.position.halfMoveClock
|
||||||
|
@ -1089,7 +1051,7 @@ proc doMove(self: ChessBoard, move: Move) =
|
||||||
self.position = Position(plyFromRoot: self.position.plyFromRoot + 1,
|
self.position = Position(plyFromRoot: self.position.plyFromRoot + 1,
|
||||||
halfMoveClock: halfMoveClock,
|
halfMoveClock: halfMoveClock,
|
||||||
fullMoveCount: fullMoveCount,
|
fullMoveCount: fullMoveCount,
|
||||||
turn: self.getSideToMove().opposite,
|
sideToMove: self.getSideToMove().opposite,
|
||||||
castlingRights: castlingRights,
|
castlingRights: castlingRights,
|
||||||
enPassantSquare: enPassantTarget,
|
enPassantSquare: enPassantTarget,
|
||||||
pieces: self.position.pieces
|
pieces: self.position.pieces
|
||||||
|
@ -1103,6 +1065,7 @@ proc doMove(self: ChessBoard, move: Move) =
|
||||||
if move.isCapture():
|
if move.isCapture():
|
||||||
# Get rid of captured pieces
|
# Get rid of captured pieces
|
||||||
self.removePiece(move.targetSquare)
|
self.removePiece(move.targetSquare)
|
||||||
|
|
||||||
# Move the piece to its target square
|
# Move the piece to its target square
|
||||||
self.movePiece(move)
|
self.movePiece(move)
|
||||||
if move.isPromotion():
|
if move.isPromotion():
|
||||||
|
@ -1121,19 +1084,19 @@ proc doMove(self: ChessBoard, move: Move) =
|
||||||
else:
|
else:
|
||||||
# Unreachable
|
# Unreachable
|
||||||
discard
|
discard
|
||||||
self.updateBoard()
|
self.updateCheckers()
|
||||||
|
|
||||||
|
|
||||||
proc spawnPiece(self: ChessBoard, square: Square, piece: Piece) =
|
proc spawnPiece(self: ChessBoard, square: Square, piece: Piece) =
|
||||||
## Internal helper to "spawn" a given piece at the given
|
## Internal helper to "spawn" a given piece at the given
|
||||||
## square. Note that this will overwrite whatever piece
|
## square
|
||||||
## was previously located there: use with caution. Does
|
when not defined(danger):
|
||||||
## not automatically update the attacked square metadata
|
doAssert self.grid[square].kind == Empty
|
||||||
self.addPieceToBitboard(square, piece)
|
self.addPieceToBitboard(square, piece)
|
||||||
self.grid[square] = piece
|
self.grid[square] = piece
|
||||||
|
|
||||||
|
|
||||||
proc updateBoard*(self: ChessBoard) =
|
proc update*(self: ChessBoard) =
|
||||||
## Updates the internal grid representation
|
## Updates the internal grid representation
|
||||||
## according to the positional data stored
|
## according to the positional data stored
|
||||||
## in the chessboard
|
## in the chessboard
|
||||||
|
@ -1168,10 +1131,8 @@ proc updateBoard*(self: ChessBoard) =
|
||||||
proc unmakeMove*(self: ChessBoard) =
|
proc unmakeMove*(self: ChessBoard) =
|
||||||
## Reverts to the previous board position,
|
## Reverts to the previous board position,
|
||||||
## if one exists
|
## if one exists
|
||||||
if self.currPos >= 0:
|
self.position = self.positions.pop()
|
||||||
self.position = self.positions[self.currPos]
|
self.update()
|
||||||
dec(self.currPos)
|
|
||||||
self.updateBoard()
|
|
||||||
|
|
||||||
|
|
||||||
proc isLegal(self: ChessBoard, move: Move): bool {.inline.} =
|
proc isLegal(self: ChessBoard, move: Move): bool {.inline.} =
|
||||||
|
@ -1329,418 +1290,6 @@ proc toFEN*(self: ChessBoard): string =
|
||||||
result &= $self.getMoveCount()
|
result &= $self.getMoveCount()
|
||||||
|
|
||||||
|
|
||||||
proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = false, bulk: bool = false): CountData =
|
|
||||||
## Counts (and debugs) the number of legal positions reached after
|
|
||||||
## the given number of ply
|
|
||||||
|
|
||||||
var moves = MoveList()
|
|
||||||
self.generateMoves(moves)
|
|
||||||
if not bulk:
|
|
||||||
if len(moves) == 0 and self.inCheck(self.getSideToMove()):
|
|
||||||
result.checkmates = 1
|
|
||||||
# TODO: Should we count stalemates/draws?
|
|
||||||
if ply == 0:
|
|
||||||
result.nodes = 1
|
|
||||||
return
|
|
||||||
elif ply == 1 and bulk:
|
|
||||||
if divide:
|
|
||||||
var postfix = ""
|
|
||||||
for move in moves:
|
|
||||||
case move.getPromotionType():
|
|
||||||
of PromoteToBishop:
|
|
||||||
postfix = "b"
|
|
||||||
of PromoteToKnight:
|
|
||||||
postfix = "n"
|
|
||||||
of PromoteToRook:
|
|
||||||
postfix = "r"
|
|
||||||
of PromoteToQueen:
|
|
||||||
postfix = "q"
|
|
||||||
else:
|
|
||||||
postfix = ""
|
|
||||||
echo &"{move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}{postfix}: 1"
|
|
||||||
if verbose:
|
|
||||||
echo ""
|
|
||||||
return (uint64(len(moves)), 0, 0, 0, 0, 0, 0)
|
|
||||||
|
|
||||||
for move in moves:
|
|
||||||
if verbose:
|
|
||||||
let canCastle = self.canCastle(self.getSideToMove())
|
|
||||||
echo &"Ply (from root): {self.position.plyFromRoot}"
|
|
||||||
echo &"Move: {move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}"
|
|
||||||
echo &"Turn: {self.getSideToMove()}"
|
|
||||||
echo &"Piece: {self.grid[move.startSquare].kind}"
|
|
||||||
echo &"Flags: {move.getFlags()}"
|
|
||||||
echo &"In check: {(if self.inCheck(self.getSideToMove()): \"yes\" else: \"no\")}"
|
|
||||||
echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
|
|
||||||
echo &"Position before move: {self.toFEN()}"
|
|
||||||
stdout.write("En Passant target: ")
|
|
||||||
if self.getEnPassantTarget() != nullSquare():
|
|
||||||
echo self.getEnPassantTarget().toAlgebraic()
|
|
||||||
else:
|
|
||||||
echo "None"
|
|
||||||
echo "\n", self.pretty()
|
|
||||||
self.doMove(move)
|
|
||||||
if ply == 1:
|
|
||||||
if move.isCapture():
|
|
||||||
inc(result.captures)
|
|
||||||
if move.isCastling():
|
|
||||||
inc(result.castles)
|
|
||||||
if move.isPromotion():
|
|
||||||
inc(result.promotions)
|
|
||||||
if move.isEnPassant():
|
|
||||||
inc(result.enPassant)
|
|
||||||
if self.inCheck(self.getSideToMove()):
|
|
||||||
# Opponent king is in check
|
|
||||||
inc(result.checks)
|
|
||||||
if verbose:
|
|
||||||
let canCastle = self.canCastle(self.getSideToMove())
|
|
||||||
echo "\n"
|
|
||||||
echo &"Opponent in check: {(if self.inCheck(self.getSideToMove()): \"yes\" else: \"no\")}"
|
|
||||||
echo &"Opponent can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
|
|
||||||
echo &"Position after move: {self.toFEN()}"
|
|
||||||
echo "\n", self.pretty()
|
|
||||||
stdout.write("nextpos>> ")
|
|
||||||
try:
|
|
||||||
discard readLine(stdin)
|
|
||||||
except IOError:
|
|
||||||
discard
|
|
||||||
except EOFError:
|
|
||||||
discard
|
|
||||||
let next = self.perft(ply - 1, verbose, bulk=bulk)
|
|
||||||
self.unmakeMove()
|
|
||||||
if divide and (not bulk or ply > 1):
|
|
||||||
var postfix = ""
|
|
||||||
if move.isPromotion():
|
|
||||||
case move.getPromotionType():
|
|
||||||
of PromoteToBishop:
|
|
||||||
postfix = "b"
|
|
||||||
of PromoteToKnight:
|
|
||||||
postfix = "n"
|
|
||||||
of PromoteToRook:
|
|
||||||
postfix = "r"
|
|
||||||
of PromoteToQueen:
|
|
||||||
postfix = "q"
|
|
||||||
else:
|
|
||||||
discard
|
|
||||||
echo &"{move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}{postfix}: {next.nodes}"
|
|
||||||
if verbose:
|
|
||||||
echo ""
|
|
||||||
result.nodes += next.nodes
|
|
||||||
result.captures += next.captures
|
|
||||||
result.checks += next.checks
|
|
||||||
result.promotions += next.promotions
|
|
||||||
result.castles += next.castles
|
|
||||||
result.enPassant += next.enPassant
|
|
||||||
result.checkmates += next.checkmates
|
|
||||||
|
|
||||||
|
|
||||||
proc handleGoCommand(board: ChessBoard, command: seq[string]) =
|
|
||||||
if len(command) < 2:
|
|
||||||
echo &"Error: go: invalid number of arguments"
|
|
||||||
return
|
|
||||||
case command[1]:
|
|
||||||
of "perft":
|
|
||||||
if len(command) == 2:
|
|
||||||
echo &"Error: go: perft: invalid number of arguments"
|
|
||||||
return
|
|
||||||
var
|
|
||||||
args = command[2].splitWhitespace()
|
|
||||||
bulk = false
|
|
||||||
verbose = false
|
|
||||||
if args.len() > 1:
|
|
||||||
var ok = true
|
|
||||||
for arg in args[1..^1]:
|
|
||||||
case arg:
|
|
||||||
of "bulk":
|
|
||||||
bulk = true
|
|
||||||
of "verbose":
|
|
||||||
verbose = true
|
|
||||||
else:
|
|
||||||
echo &"Error: go: perft: invalid argument '{args[1]}'"
|
|
||||||
ok = false
|
|
||||||
break
|
|
||||||
if not ok:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
let ply = parseInt(args[0])
|
|
||||||
if bulk:
|
|
||||||
let t = cpuTime()
|
|
||||||
let nodes = board.perft(ply, divide=true, bulk=true, verbose=verbose).nodes
|
|
||||||
echo &"\nNodes searched (bulk-counting: on): {nodes}"
|
|
||||||
echo &"Time taken: {round(cpuTime() - t, 3)} seconds\n"
|
|
||||||
else:
|
|
||||||
let t = cpuTime()
|
|
||||||
let data = board.perft(ply, divide=true, verbose=verbose)
|
|
||||||
echo &"\nNodes searched (bulk-counting: off): {data.nodes}"
|
|
||||||
echo &" - Captures: {data.captures}"
|
|
||||||
echo &" - Checks: {data.checks}"
|
|
||||||
echo &" - E.P: {data.enPassant}"
|
|
||||||
echo &" - Checkmates: {data.checkmates}"
|
|
||||||
echo &" - Castles: {data.castles}"
|
|
||||||
echo &" - Promotions: {data.promotions}"
|
|
||||||
echo ""
|
|
||||||
echo &"Time taken: {round(cpuTime() - t, 3)} seconds"
|
|
||||||
except ValueError:
|
|
||||||
echo "Error: go: perft: invalid depth"
|
|
||||||
else:
|
|
||||||
echo &"Error: go: unknown subcommand '{command[1]}'"
|
|
||||||
|
|
||||||
|
|
||||||
proc handleMoveCommand(board: ChessBoard, command: seq[string]): Move {.discardable.} =
|
|
||||||
if len(command) != 2:
|
|
||||||
echo &"Error: move: invalid number of arguments"
|
|
||||||
return
|
|
||||||
let moveString = command[1]
|
|
||||||
if len(moveString) notin 4..5:
|
|
||||||
echo &"Error: move: invalid move syntax"
|
|
||||||
return
|
|
||||||
var
|
|
||||||
startSquare: Square
|
|
||||||
targetSquare: Square
|
|
||||||
flags: seq[MoveFlag]
|
|
||||||
|
|
||||||
try:
|
|
||||||
startSquare = moveString[0..1].toSquare()
|
|
||||||
except ValueError:
|
|
||||||
echo &"Error: move: invalid start square ({moveString[0..1]})"
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
targetSquare = moveString[2..3].toSquare()
|
|
||||||
except ValueError:
|
|
||||||
echo &"Error: move: invalid target square ({moveString[2..3]})"
|
|
||||||
return
|
|
||||||
|
|
||||||
# Since the user tells us just the source and target square of the move,
|
|
||||||
# we have to figure out all the flags by ourselves (whether it's a double
|
|
||||||
# push, a capture, a promotion, etc.)
|
|
||||||
|
|
||||||
if board.grid[targetSquare].kind != Empty:
|
|
||||||
flags.add(Capture)
|
|
||||||
|
|
||||||
elif board.grid[startSquare].kind == Pawn and abs(rankFromSquare(startSquare) - rankFromSquare(targetSquare)) == 2:
|
|
||||||
flags.add(DoublePush)
|
|
||||||
|
|
||||||
if len(moveString) == 5:
|
|
||||||
# Promotion
|
|
||||||
case moveString[4]:
|
|
||||||
of 'b':
|
|
||||||
flags.add(PromoteToBishop)
|
|
||||||
of 'n':
|
|
||||||
flags.add(PromoteToKnight)
|
|
||||||
of 'q':
|
|
||||||
flags.add(PromoteToQueen)
|
|
||||||
of 'r':
|
|
||||||
flags.add(PromoteToRook)
|
|
||||||
else:
|
|
||||||
echo &"Error: move: invalid promotion type"
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
var move = createMove(startSquare, targetSquare, flags)
|
|
||||||
let piece = board.getPiece(move.startSquare)
|
|
||||||
if piece.kind == King and move.startSquare == board.getSideToMove().getKingStartingSquare():
|
|
||||||
if move.targetSquare == longCastleKing(piece.color):
|
|
||||||
move.flags = move.flags or CastleLong.uint16
|
|
||||||
elif move.targetSquare == shortCastleKing(piece.color):
|
|
||||||
move.flags = move.flags or CastleShort.uint16
|
|
||||||
if move.targetSquare == board.getEnPassantTarget():
|
|
||||||
move.flags = move.flags or EnPassant.uint16
|
|
||||||
result = board.makeMove(move)
|
|
||||||
if result == nullMove():
|
|
||||||
echo &"Error: move: {moveString} is illegal"
|
|
||||||
|
|
||||||
|
|
||||||
proc handlePositionCommand(board: var ChessBoard, command: seq[string]) =
|
|
||||||
if len(command) < 2:
|
|
||||||
echo "Error: position: invalid number of arguments"
|
|
||||||
return
|
|
||||||
# Makes sure we don't leave the board in an invalid state if
|
|
||||||
# some error occurs
|
|
||||||
var tempBoard: ChessBoard
|
|
||||||
case command[1]:
|
|
||||||
of "startpos":
|
|
||||||
tempBoard = newDefaultChessboard()
|
|
||||||
if command.len() > 2:
|
|
||||||
let args = command[2].splitWhitespace()
|
|
||||||
if args.len() > 0:
|
|
||||||
var i = 0
|
|
||||||
while i < args.len():
|
|
||||||
case args[i]:
|
|
||||||
of "moves":
|
|
||||||
var j = i + 1
|
|
||||||
while j < args.len():
|
|
||||||
if handleMoveCommand(tempBoard, @["move", args[j]]) == nullMove():
|
|
||||||
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]]) == nullMove():
|
|
||||||
return
|
|
||||||
inc(j)
|
|
||||||
inc(i)
|
|
||||||
board = tempBoard
|
|
||||||
of "print":
|
|
||||||
echo board
|
|
||||||
of "pretty":
|
|
||||||
echo board.pretty()
|
|
||||||
else:
|
|
||||||
echo &"error: position: unknown subcommand '{command[1]}'"
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
proc handleUCICommand(board: var ChessBoard, command: seq[string]) =
|
|
||||||
echo "id name Nimfish 0.1"
|
|
||||||
echo "id author Nocturn9x & Contributors (see LICENSE)"
|
|
||||||
# TODO
|
|
||||||
echo "uciok"
|
|
||||||
|
|
||||||
|
|
||||||
const HELP_TEXT = """Nimfish help menu:
|
|
||||||
- go: Begin a search
|
|
||||||
Subcommands:
|
|
||||||
- perft <depth> [options]: Run the performance test at the given depth (in ply) and
|
|
||||||
print the results
|
|
||||||
Options:
|
|
||||||
- bulk: Enable bulk-counting (significantly faster, gives less statistics)
|
|
||||||
- verbose: Enable move debugging (for each and every move, not recommended on large searches)
|
|
||||||
Example: go perft 5 bulk
|
|
||||||
- position: Get/set board position
|
|
||||||
Subcommands:
|
|
||||||
- fen [string]: Set the board to the given fen string if one is provided, or print
|
|
||||||
the current position as a FEN string if no arguments are given
|
|
||||||
- startpos: Set the board to the starting position
|
|
||||||
- pretty: Pretty-print the current position
|
|
||||||
- print: Print the current position using ASCII characters only
|
|
||||||
Options:
|
|
||||||
- moves {moveList}: Perform the given moves (space-separated, all-lowercase)
|
|
||||||
in algebraic notation after the position is loaded. This option only applies
|
|
||||||
to the "startpos" and "fen" subcommands: it is ignored otherwise
|
|
||||||
Examples:
|
|
||||||
- position startpos
|
|
||||||
- position fen "..." moves a2a3 a7a6
|
|
||||||
- clear: Clear the screen
|
|
||||||
- move <move>: Perform the given move in algebraic notation
|
|
||||||
- castle: Print castlingRights rights for each side
|
|
||||||
- check: Print if the current side to move is in check
|
|
||||||
- undo, u: Undoes the last move. Can be used in succession
|
|
||||||
- turn: Print which side is to move
|
|
||||||
- ep: Print the current en passant target
|
|
||||||
- pretty: Shorthand for "position pretty"
|
|
||||||
- print: Shorthand for "position print"
|
|
||||||
- fen: Shorthand for "position fen"
|
|
||||||
- pos <args>: Shorthand for "position <args>"
|
|
||||||
- get <square>: Get the piece on the given square
|
|
||||||
- atk: Print the attack bitboard of the given square for the side to move
|
|
||||||
- uci: enter UCI mode (WIP)
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
proc main: int =
|
|
||||||
## Nimfish's control interface
|
|
||||||
echo "Nimfish by nocturn9x (see LICENSE)"
|
|
||||||
var
|
|
||||||
board = newDefaultChessboard()
|
|
||||||
uciMode = false
|
|
||||||
while true:
|
|
||||||
var
|
|
||||||
cmd: seq[string]
|
|
||||||
cmdStr: string
|
|
||||||
try:
|
|
||||||
if not uciMode:
|
|
||||||
stdout.write(">>> ")
|
|
||||||
stdout.flushFile()
|
|
||||||
cmdStr = readLine(stdin).strip(leading=true, trailing=true, chars={'\t', ' '})
|
|
||||||
if cmdStr.len() == 0:
|
|
||||||
continue
|
|
||||||
cmd = cmdStr.splitWhitespace(maxsplit=2)
|
|
||||||
|
|
||||||
case cmd[0]:
|
|
||||||
of "uci":
|
|
||||||
handleUCICommand(board, cmd)
|
|
||||||
uciMode = true
|
|
||||||
of "clear":
|
|
||||||
echo "\x1Bc"
|
|
||||||
of "help":
|
|
||||||
echo HELP_TEXT
|
|
||||||
of "go":
|
|
||||||
handleGoCommand(board, cmd)
|
|
||||||
of "position", "pos":
|
|
||||||
handlePositionCommand(board, cmd)
|
|
||||||
of "move":
|
|
||||||
handleMoveCommand(board, cmd)
|
|
||||||
of "pretty", "print", "fen":
|
|
||||||
handlePositionCommand(board, @["position", cmd[0]])
|
|
||||||
of "undo", "u":
|
|
||||||
board.unmakeMove()
|
|
||||||
of "turn":
|
|
||||||
echo &"Active color: {board.getSideToMove()}"
|
|
||||||
of "atk":
|
|
||||||
if len(cmd) != 2:
|
|
||||||
echo "error: atk: invalid number of arguments"
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
echo board.getAttacksTo(cmd[1].toSquare(), board.getSideToMove())
|
|
||||||
except ValueError:
|
|
||||||
echo "error: atk: invalid square"
|
|
||||||
continue
|
|
||||||
of "ep":
|
|
||||||
let target = board.getEnPassantTarget()
|
|
||||||
if target != nullSquare():
|
|
||||||
echo &"En passant target: {target.toAlgebraic()}"
|
|
||||||
else:
|
|
||||||
echo "En passant target: None"
|
|
||||||
of "get":
|
|
||||||
if len(cmd) != 2:
|
|
||||||
echo "error: get: invalid number of arguments"
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
echo board.getPiece(cmd[1])
|
|
||||||
except ValueError:
|
|
||||||
echo "error: get: invalid square"
|
|
||||||
continue
|
|
||||||
of "castle":
|
|
||||||
let canCastle = board.canCastle(board.getSideToMove())
|
|
||||||
echo &"Castling rights for {($board.getSideToMove()).toLowerAscii()}:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
|
|
||||||
of "check":
|
|
||||||
echo &"{board.getSideToMove()} king in check: {(if board.inCheck(board.getSideToMove()): \"yes\" else: \"no\")}"
|
|
||||||
else:
|
|
||||||
echo &"Unknown command '{cmd[0]}'. Type 'help' for more information."
|
|
||||||
except IOError:
|
|
||||||
echo ""
|
|
||||||
return 0
|
|
||||||
except EOFError:
|
|
||||||
echo ""
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
when isMainModule:
|
when isMainModule:
|
||||||
|
|
||||||
|
|
||||||
|
@ -1846,5 +1395,9 @@ when isMainModule:
|
||||||
testPieceBitboard(blackQueens, blackQueenSquares)
|
testPieceBitboard(blackQueens, blackQueenSquares)
|
||||||
testPieceBitboard(blackKing, blackKingSquares)
|
testPieceBitboard(blackKing, blackKingSquares)
|
||||||
|
|
||||||
|
|
||||||
setControlCHook(proc () {.noconv.} = quit(0))
|
setControlCHook(proc () {.noconv.} = quit(0))
|
||||||
quit(main())
|
|
||||||
|
import tui
|
||||||
|
|
||||||
|
quit(tui.commandLoop())
|
||||||
|
|
|
@ -0,0 +1,427 @@
|
||||||
|
import nimfish
|
||||||
|
|
||||||
|
|
||||||
|
import std/strformat
|
||||||
|
import std/strutils
|
||||||
|
import std/times
|
||||||
|
import std/math
|
||||||
|
|
||||||
|
|
||||||
|
type
|
||||||
|
CountData = tuple[nodes: uint64, captures: uint64, castles: uint64, checks: uint64, promotions: uint64, enPassant: uint64, checkmates: uint64]
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
var moves = MoveList()
|
||||||
|
self.generateMoves(moves)
|
||||||
|
if not bulk:
|
||||||
|
if len(moves) == 0 and self.inCheck(self.getSideToMove()):
|
||||||
|
result.checkmates = 1
|
||||||
|
# TODO: Should we count stalemates/draws?
|
||||||
|
if ply == 0:
|
||||||
|
result.nodes = 1
|
||||||
|
return
|
||||||
|
elif ply == 1 and bulk:
|
||||||
|
if divide:
|
||||||
|
var postfix = ""
|
||||||
|
for move in moves:
|
||||||
|
case move.getPromotionType():
|
||||||
|
of PromoteToBishop:
|
||||||
|
postfix = "b"
|
||||||
|
of PromoteToKnight:
|
||||||
|
postfix = "n"
|
||||||
|
of PromoteToRook:
|
||||||
|
postfix = "r"
|
||||||
|
of PromoteToQueen:
|
||||||
|
postfix = "q"
|
||||||
|
else:
|
||||||
|
postfix = ""
|
||||||
|
echo &"{move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}{postfix}: 1"
|
||||||
|
if verbose:
|
||||||
|
echo ""
|
||||||
|
return (uint64(len(moves)), 0, 0, 0, 0, 0, 0)
|
||||||
|
|
||||||
|
for move in moves:
|
||||||
|
if verbose:
|
||||||
|
let canCastle = self.canCastle(self.getSideToMove())
|
||||||
|
echo &"Ply (from root): {self.getPlyFromRoot()}"
|
||||||
|
echo &"Move: {move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}"
|
||||||
|
echo &"Turn: {self.getSideToMove()}"
|
||||||
|
echo &"Piece: {self.getPiece(move.startSquare).kind}"
|
||||||
|
echo &"Flags: {move.getFlags()}"
|
||||||
|
echo &"In check: {(if self.inCheck(self.getSideToMove()): \"yes\" else: \"no\")}"
|
||||||
|
echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
|
||||||
|
echo &"Position before move: {self.toFEN()}"
|
||||||
|
stdout.write("En Passant target: ")
|
||||||
|
if self.getEnPassantTarget() != nullSquare():
|
||||||
|
echo self.getEnPassantTarget().toAlgebraic()
|
||||||
|
else:
|
||||||
|
echo "None"
|
||||||
|
echo "\n", self.pretty()
|
||||||
|
self.doMove(move)
|
||||||
|
if ply == 1:
|
||||||
|
if move.isCapture():
|
||||||
|
inc(result.captures)
|
||||||
|
if move.isCastling():
|
||||||
|
inc(result.castles)
|
||||||
|
if move.isPromotion():
|
||||||
|
inc(result.promotions)
|
||||||
|
if move.isEnPassant():
|
||||||
|
inc(result.enPassant)
|
||||||
|
if self.inCheck(self.getSideToMove()):
|
||||||
|
# Opponent king is in check
|
||||||
|
inc(result.checks)
|
||||||
|
if verbose:
|
||||||
|
let canCastle = self.canCastle(self.getSideToMove())
|
||||||
|
echo "\n"
|
||||||
|
echo &"Opponent in check: {(if self.inCheck(self.getSideToMove()): \"yes\" else: \"no\")}"
|
||||||
|
echo &"Opponent can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
|
||||||
|
echo &"Position after move: {self.toFEN()}"
|
||||||
|
echo "\n", self.pretty()
|
||||||
|
stdout.write("nextpos>> ")
|
||||||
|
try:
|
||||||
|
discard readLine(stdin)
|
||||||
|
except IOError:
|
||||||
|
discard
|
||||||
|
except EOFError:
|
||||||
|
discard
|
||||||
|
let next = self.perft(ply - 1, verbose, bulk=bulk)
|
||||||
|
self.unmakeMove()
|
||||||
|
if divide and (not bulk or ply > 1):
|
||||||
|
var postfix = ""
|
||||||
|
if move.isPromotion():
|
||||||
|
case move.getPromotionType():
|
||||||
|
of PromoteToBishop:
|
||||||
|
postfix = "b"
|
||||||
|
of PromoteToKnight:
|
||||||
|
postfix = "n"
|
||||||
|
of PromoteToRook:
|
||||||
|
postfix = "r"
|
||||||
|
of PromoteToQueen:
|
||||||
|
postfix = "q"
|
||||||
|
else:
|
||||||
|
discard
|
||||||
|
echo &"{move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}{postfix}: {next.nodes}"
|
||||||
|
if verbose:
|
||||||
|
echo ""
|
||||||
|
result.nodes += next.nodes
|
||||||
|
result.captures += next.captures
|
||||||
|
result.checks += next.checks
|
||||||
|
result.promotions += next.promotions
|
||||||
|
result.castles += next.castles
|
||||||
|
result.enPassant += next.enPassant
|
||||||
|
result.checkmates += next.checkmates
|
||||||
|
|
||||||
|
|
||||||
|
proc handleGoCommand(board: ChessBoard, command: seq[string]) =
|
||||||
|
if len(command) < 2:
|
||||||
|
echo &"Error: go: invalid number of arguments"
|
||||||
|
return
|
||||||
|
case command[1]:
|
||||||
|
of "perft":
|
||||||
|
if len(command) == 2:
|
||||||
|
echo &"Error: go: perft: invalid number of arguments"
|
||||||
|
return
|
||||||
|
var
|
||||||
|
args = command[2].splitWhitespace()
|
||||||
|
bulk = false
|
||||||
|
verbose = false
|
||||||
|
if args.len() > 1:
|
||||||
|
var ok = true
|
||||||
|
for arg in args[1..^1]:
|
||||||
|
case arg:
|
||||||
|
of "bulk":
|
||||||
|
bulk = true
|
||||||
|
of "verbose":
|
||||||
|
verbose = true
|
||||||
|
else:
|
||||||
|
echo &"Error: go: perft: invalid argument '{args[1]}'"
|
||||||
|
ok = false
|
||||||
|
break
|
||||||
|
if not ok:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
let ply = parseInt(args[0])
|
||||||
|
if bulk:
|
||||||
|
let t = cpuTime()
|
||||||
|
let nodes = board.perft(ply, divide=true, bulk=true, verbose=verbose).nodes
|
||||||
|
echo &"\nNodes searched (bulk-counting: on): {nodes}"
|
||||||
|
echo &"Time taken: {round(cpuTime() - t, 3)} seconds\n"
|
||||||
|
else:
|
||||||
|
let t = cpuTime()
|
||||||
|
let data = board.perft(ply, divide=true, verbose=verbose)
|
||||||
|
echo &"\nNodes searched (bulk-counting: off): {data.nodes}"
|
||||||
|
echo &" - Captures: {data.captures}"
|
||||||
|
echo &" - Checks: {data.checks}"
|
||||||
|
echo &" - E.P: {data.enPassant}"
|
||||||
|
echo &" - Checkmates: {data.checkmates}"
|
||||||
|
echo &" - Castles: {data.castles}"
|
||||||
|
echo &" - Promotions: {data.promotions}"
|
||||||
|
echo ""
|
||||||
|
echo &"Time taken: {round(cpuTime() - t, 3)} seconds"
|
||||||
|
except ValueError:
|
||||||
|
echo "Error: go: perft: invalid depth"
|
||||||
|
else:
|
||||||
|
echo &"Error: go: unknown subcommand '{command[1]}'"
|
||||||
|
|
||||||
|
|
||||||
|
proc handleMoveCommand(board: ChessBoard, command: seq[string]): Move {.discardable.} =
|
||||||
|
if len(command) != 2:
|
||||||
|
echo &"Error: move: invalid number of arguments"
|
||||||
|
return
|
||||||
|
let moveString = command[1]
|
||||||
|
if len(moveString) notin 4..5:
|
||||||
|
echo &"Error: move: invalid move syntax"
|
||||||
|
return
|
||||||
|
var
|
||||||
|
startSquare: Square
|
||||||
|
targetSquare: Square
|
||||||
|
flags: seq[MoveFlag]
|
||||||
|
|
||||||
|
try:
|
||||||
|
startSquare = moveString[0..1].toSquare()
|
||||||
|
except ValueError:
|
||||||
|
echo &"Error: move: invalid start square ({moveString[0..1]})"
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
targetSquare = moveString[2..3].toSquare()
|
||||||
|
except ValueError:
|
||||||
|
echo &"Error: move: invalid target square ({moveString[2..3]})"
|
||||||
|
return
|
||||||
|
|
||||||
|
# Since the user tells us just the source and target square of the move,
|
||||||
|
# we have to figure out all the flags by ourselves (whether it's a double
|
||||||
|
# push, a capture, a promotion, etc.)
|
||||||
|
|
||||||
|
if board.getPiece(targetSquare).kind != Empty:
|
||||||
|
flags.add(Capture)
|
||||||
|
|
||||||
|
elif board.getPiece(targetSquare).kind == Pawn and abs(rankFromSquare(startSquare) - rankFromSquare(targetSquare)) == 2:
|
||||||
|
flags.add(DoublePush)
|
||||||
|
|
||||||
|
if len(moveString) == 5:
|
||||||
|
# Promotion
|
||||||
|
case moveString[4]:
|
||||||
|
of 'b':
|
||||||
|
flags.add(PromoteToBishop)
|
||||||
|
of 'n':
|
||||||
|
flags.add(PromoteToKnight)
|
||||||
|
of 'q':
|
||||||
|
flags.add(PromoteToQueen)
|
||||||
|
of 'r':
|
||||||
|
flags.add(PromoteToRook)
|
||||||
|
else:
|
||||||
|
echo &"Error: move: invalid promotion type"
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
var move = createMove(startSquare, targetSquare, flags)
|
||||||
|
let piece = board.getPiece(move.startSquare)
|
||||||
|
if piece.kind == King and move.startSquare == board.getSideToMove().getKingStartingSquare():
|
||||||
|
if move.targetSquare == longCastleKing(piece.color):
|
||||||
|
move.flags = move.flags or CastleLong.uint16
|
||||||
|
elif move.targetSquare == shortCastleKing(piece.color):
|
||||||
|
move.flags = move.flags or CastleShort.uint16
|
||||||
|
if move.targetSquare == board.getEnPassantTarget():
|
||||||
|
move.flags = move.flags or EnPassant.uint16
|
||||||
|
result = board.makeMove(move)
|
||||||
|
if result == nullMove():
|
||||||
|
echo &"Error: move: {moveString} is illegal"
|
||||||
|
|
||||||
|
|
||||||
|
proc handlePositionCommand(board: var ChessBoard, command: seq[string]) =
|
||||||
|
if len(command) < 2:
|
||||||
|
echo "Error: position: invalid number of arguments"
|
||||||
|
return
|
||||||
|
# Makes sure we don't leave the board in an invalid state if
|
||||||
|
# some error occurs
|
||||||
|
var tempBoard: ChessBoard
|
||||||
|
case command[1]:
|
||||||
|
of "startpos":
|
||||||
|
tempBoard = newDefaultChessboard()
|
||||||
|
if command.len() > 2:
|
||||||
|
let args = command[2].splitWhitespace()
|
||||||
|
if args.len() > 0:
|
||||||
|
var i = 0
|
||||||
|
while i < args.len():
|
||||||
|
case args[i]:
|
||||||
|
of "moves":
|
||||||
|
var j = i + 1
|
||||||
|
while j < args.len():
|
||||||
|
if handleMoveCommand(tempBoard, @["move", args[j]]) == nullMove():
|
||||||
|
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]]) == nullMove():
|
||||||
|
return
|
||||||
|
inc(j)
|
||||||
|
inc(i)
|
||||||
|
board = tempBoard
|
||||||
|
of "print":
|
||||||
|
echo board
|
||||||
|
of "pretty":
|
||||||
|
echo board.pretty()
|
||||||
|
else:
|
||||||
|
echo &"error: position: unknown subcommand '{command[1]}'"
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
proc handleUCICommand(board: var ChessBoard, command: seq[string]) =
|
||||||
|
echo "id name Nimfish 0.1"
|
||||||
|
echo "id author Nocturn9x & Contributors (see LICENSE)"
|
||||||
|
# TODO
|
||||||
|
echo "uciok"
|
||||||
|
|
||||||
|
|
||||||
|
const HELP_TEXT = """Nimfish help menu:
|
||||||
|
- go: Begin a search
|
||||||
|
Subcommands:
|
||||||
|
- perft <depth> [options]: Run the performance test at the given depth (in ply) and
|
||||||
|
print the results
|
||||||
|
Options:
|
||||||
|
- bulk: Enable bulk-counting (significantly faster, gives less statistics)
|
||||||
|
- verbose: Enable move debugging (for each and every move, not recommended on large searches)
|
||||||
|
Example: go perft 5 bulk
|
||||||
|
- position: Get/set board position
|
||||||
|
Subcommands:
|
||||||
|
- fen [string]: Set the board to the given fen string if one is provided, or print
|
||||||
|
the current position as a FEN string if no arguments are given
|
||||||
|
- startpos: Set the board to the starting position
|
||||||
|
- pretty: Pretty-print the current position
|
||||||
|
- print: Print the current position using ASCII characters only
|
||||||
|
Options:
|
||||||
|
- moves {moveList}: Perform the given moves (space-separated, all-lowercase)
|
||||||
|
in algebraic notation after the position is loaded. This option only applies
|
||||||
|
to the "startpos" and "fen" subcommands: it is ignored otherwise
|
||||||
|
Examples:
|
||||||
|
- position startpos
|
||||||
|
- position fen "..." moves a2a3 a7a6
|
||||||
|
- clear: Clear the screen
|
||||||
|
- move <move>: Perform the given move in algebraic notation
|
||||||
|
- castle: Print castlingRights rights for each side
|
||||||
|
- check: Print if the current side to move is in check
|
||||||
|
- unmove, u: Unmakes the last move. Can be used in succession
|
||||||
|
- stm: Print which side is to move
|
||||||
|
- ep: Print the current en passant target
|
||||||
|
- pretty: Shorthand for "position pretty"
|
||||||
|
- print: Shorthand for "position print"
|
||||||
|
- fen: Shorthand for "position fen"
|
||||||
|
- pos <args>: Shorthand for "position <args>"
|
||||||
|
- get <square>: Get the piece on the given square
|
||||||
|
- atk <square>: Print the attack bitboard of the given square for the side to move
|
||||||
|
- skip: Swap the side to move
|
||||||
|
- uci: enter UCI mode (WIP)
|
||||||
|
- quit: exit
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
proc commandLoop*: int =
|
||||||
|
## Nimfish's control interface
|
||||||
|
echo "Nimfish by nocturn9x (see LICENSE)"
|
||||||
|
var
|
||||||
|
board = newDefaultChessboard()
|
||||||
|
uciMode = false
|
||||||
|
while true:
|
||||||
|
var
|
||||||
|
cmd: seq[string]
|
||||||
|
cmdStr: string
|
||||||
|
try:
|
||||||
|
if not uciMode:
|
||||||
|
stdout.write(">>> ")
|
||||||
|
stdout.flushFile()
|
||||||
|
cmdStr = readLine(stdin).strip(leading=true, trailing=true, chars={'\t', ' '})
|
||||||
|
if cmdStr.len() == 0:
|
||||||
|
continue
|
||||||
|
cmd = cmdStr.splitWhitespace(maxsplit=2)
|
||||||
|
|
||||||
|
case cmd[0]:
|
||||||
|
of "uci":
|
||||||
|
handleUCICommand(board, cmd)
|
||||||
|
uciMode = true
|
||||||
|
of "clear":
|
||||||
|
echo "\x1Bc"
|
||||||
|
of "help":
|
||||||
|
echo HELP_TEXT
|
||||||
|
of "skip":
|
||||||
|
board.setSideToMove(board.getSideToMove().opposite())
|
||||||
|
of "go":
|
||||||
|
handleGoCommand(board, cmd)
|
||||||
|
of "position", "pos":
|
||||||
|
handlePositionCommand(board, cmd)
|
||||||
|
of "move":
|
||||||
|
handleMoveCommand(board, cmd)
|
||||||
|
of "pretty", "print", "fen":
|
||||||
|
handlePositionCommand(board, @["position", cmd[0]])
|
||||||
|
of "unmove", "u":
|
||||||
|
board.unmakeMove()
|
||||||
|
of "stm":
|
||||||
|
echo &"Side to move: {board.getSideToMove()}"
|
||||||
|
of "atk":
|
||||||
|
if len(cmd) != 2:
|
||||||
|
echo "error: atk: invalid number of arguments"
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
echo board.getAttacksTo(cmd[1].toSquare(), board.getSideToMove())
|
||||||
|
except ValueError:
|
||||||
|
echo "error: atk: invalid square"
|
||||||
|
continue
|
||||||
|
of "ep":
|
||||||
|
let target = board.getEnPassantTarget()
|
||||||
|
if target != nullSquare():
|
||||||
|
echo &"En passant target: {target.toAlgebraic()}"
|
||||||
|
else:
|
||||||
|
echo "En passant target: None"
|
||||||
|
of "get":
|
||||||
|
if len(cmd) != 2:
|
||||||
|
echo "error: get: invalid number of arguments"
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
echo board.getPiece(cmd[1])
|
||||||
|
except ValueError:
|
||||||
|
echo "error: get: invalid square"
|
||||||
|
continue
|
||||||
|
of "castle":
|
||||||
|
let canCastle = board.canCastle(board.getSideToMove())
|
||||||
|
echo &"Castling rights for {($board.getSideToMove()).toLowerAscii()}:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
|
||||||
|
of "check":
|
||||||
|
echo &"{board.getSideToMove()} king in check: {(if board.inCheck(board.getSideToMove()): \"yes\" else: \"no\")}"
|
||||||
|
else:
|
||||||
|
echo &"Unknown command '{cmd[0]}'. Type 'help' for more information."
|
||||||
|
except IOError:
|
||||||
|
echo ""
|
||||||
|
return 0
|
||||||
|
except EOFError:
|
||||||
|
echo ""
|
||||||
|
return 0
|
Loading…
Reference in New Issue