462 lines
18 KiB
Nim
462 lines
18 KiB
Nim
# Copyright 2024 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 movegen
|
|
import uci
|
|
|
|
|
|
import std/strformat
|
|
import std/strutils
|
|
import std/times
|
|
import std/math
|
|
|
|
from std/lenientops import `/`
|
|
|
|
|
|
type
|
|
CountData = tuple[nodes: uint64, captures: uint64, castles: uint64, checks: uint64, promotions: uint64, enPassant: uint64, checkmates: uint64]
|
|
|
|
|
|
proc perft*(board: 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()
|
|
board.generateMoves(moves)
|
|
if not bulk:
|
|
if len(moves) == 0 and board.inCheck():
|
|
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 = board.canCastle()
|
|
echo &"Ply (from root): {board.position.plyFromRoot}"
|
|
echo &"Move: {move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}"
|
|
echo &"Turn: {board.position.sideToMove}"
|
|
echo &"Piece: {board.getPiece(move.startSquare).kind}"
|
|
echo &"Flags: {move.getFlags()}"
|
|
echo &"In check: {(if board.inCheck(): \"yes\" else: \"no\")}"
|
|
echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
|
|
echo &"Position before move: {board.toFEN()}"
|
|
stdout.write("En Passant target: ")
|
|
if board.position.enPassantSquare != nullSquare():
|
|
echo board.position.enPassantSquare.toAlgebraic()
|
|
else:
|
|
echo "None"
|
|
echo "\n", board.pretty()
|
|
board.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 board.inCheck():
|
|
# Opponent king is in check
|
|
inc(result.checks)
|
|
if verbose:
|
|
let canCastle = board.canCastle()
|
|
echo "\n"
|
|
echo &"Opponent in check: {(if board.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: {board.toFEN()}"
|
|
echo "\n", board.pretty()
|
|
stdout.write("nextpos>> ")
|
|
try:
|
|
discard readLine(stdin)
|
|
except IOError:
|
|
discard
|
|
except EOFError:
|
|
discard
|
|
let next = board.perft(ply - 1, verbose, bulk=bulk)
|
|
board.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
|
|
let tot = cpuTime() - t
|
|
echo &"\nNodes searched (bulk-counting: on): {nodes}"
|
|
echo &"Time taken: {round(tot, 3)} seconds\nNodes per second: {round(nodes / tot).uint64}"
|
|
else:
|
|
let t = cpuTime()
|
|
let data = board.perft(ply, divide=true, verbose=verbose)
|
|
let tot = cpuTime() - t
|
|
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(tot, 3)} seconds\nNodes per second: {round(data.nodes / tot).uint64}"
|
|
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)
|
|
|
|
if board.getPiece(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.position.sideToMove.getKingStartingSquare():
|
|
if move.targetSquare in [piece.kingSideCastling(), piece.queenSideCastling()]:
|
|
move.flags = move.flags or Castle.uint16
|
|
elif move.targetSquare == board.position.enPassantSquare:
|
|
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", "kiwipete":
|
|
if command[1] == "kiwipete":
|
|
tempBoard = newChessboardFromFen("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -")
|
|
else:
|
|
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
|
|
|
|
|
|
|
|
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
|
|
- kiwipete: Set the board to famous kiwipete 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 subcommands that set a position, 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 the side to move
|
|
- 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 which pieces are currently attacking the given square
|
|
- pins: Print the current pin mask
|
|
- checks: Print the current checks mask
|
|
- skip: Swap the side to move
|
|
- uci: enter UCI mode
|
|
- quit: exit
|
|
- zobrist: Print the zobrist key for the current position
|
|
"""
|
|
|
|
|
|
proc commandLoop*: int =
|
|
## Nimfish's control interface
|
|
echo "Nimfish by nocturn9x (see LICENSE)"
|
|
var
|
|
board = newDefaultChessboard()
|
|
startUCI = false
|
|
while true:
|
|
var
|
|
cmd: seq[string]
|
|
cmdStr: string
|
|
try:
|
|
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":
|
|
startUCI = true
|
|
break
|
|
of "clear":
|
|
echo "\x1Bc"
|
|
of "help":
|
|
echo HELP_TEXT
|
|
of "skip":
|
|
board.position.sideToMove = board.position.sideToMove.opposite()
|
|
board.updateChecksAndPins()
|
|
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":
|
|
if board.positions.len() == 0:
|
|
echo "No previous move to undo"
|
|
else:
|
|
board.unmakeMove()
|
|
of "stm":
|
|
echo &"Side to move: {board.position.sideToMove}"
|
|
of "atk":
|
|
if len(cmd) != 2:
|
|
echo "error: atk: invalid number of arguments"
|
|
continue
|
|
try:
|
|
echo board.getAttacksTo(cmd[1].toSquare(), board.position.sideToMove.opposite())
|
|
except ValueError:
|
|
echo "error: atk: invalid square"
|
|
continue
|
|
of "ep":
|
|
let target = board.position.enPassantSquare
|
|
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()
|
|
echo &"Castling rights for {($board.position.sideToMove).toLowerAscii()}:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
|
|
of "check":
|
|
echo &"{board.position.sideToMove} king in check: {(if board.inCheck(): \"yes\" else: \"no\")}"
|
|
of "pins":
|
|
if board.position.orthogonalPins != 0:
|
|
echo &"Orthogonal pins:\n{board.position.orthogonalPins}"
|
|
if board.position.diagonalPins != 0:
|
|
echo &"Diagonal pins:\n{board.position.diagonalPins}"
|
|
of "checks":
|
|
echo board.position.checkers
|
|
of "quit":
|
|
return 0
|
|
of "zobrist":
|
|
echo board.position.zobristKey.uint64
|
|
of "rep":
|
|
echo board.position.repetitionDraw
|
|
else:
|
|
echo &"Unknown command '{cmd[0]}'. Type 'help' for more information."
|
|
except IOError:
|
|
echo ""
|
|
return 0
|
|
except EOFError:
|
|
echo ""
|
|
return 0
|
|
if startUCI:
|
|
startUCISession() |