Compare commits
2 Commits
ce960003a2
...
720645092e
Author | SHA1 | Date |
---|---|---|
Mattia Giambirtone | 720645092e | |
Mattia Giambirtone | 629718a54c |
|
@ -3,3 +3,4 @@
|
|||
-d:danger
|
||||
--passL:"-flto"
|
||||
--passC:"-Ofast -flto -march=native -mtune=native"
|
||||
--threads:on
|
|
@ -222,7 +222,7 @@ proc removePiece*(self: Chessboard, square: Square) =
|
|||
## Removes a piece from the board, updating necessary
|
||||
## metadata
|
||||
when not defined(danger):
|
||||
let Piece = self.getPiece(square)
|
||||
let piece = self.getPiece(square)
|
||||
doAssert piece.kind != Empty and piece.color != None, self.toFEN()
|
||||
self.removePieceFromBitboard(square)
|
||||
self.grid[square] = nullPiece()
|
||||
|
|
|
@ -0,0 +1,279 @@
|
|||
# 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.
|
||||
|
||||
## Position evaluation utilities
|
||||
import board
|
||||
|
||||
type
|
||||
Score* = int32
|
||||
|
||||
|
||||
# Stolen from https://www.chessprogramming.org/PeSTO's_Evaluation_Function
|
||||
const
|
||||
PAWN_MIDDLEGAME_SCORES: array[64, Score] = [
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
98, 134, 61, 95, 68, 126, 34, -11,
|
||||
-6, 7, 26, 31, 65, 56, 25, -20,
|
||||
-14, 13, 6, 21, 23, 12, 17, -23,
|
||||
-27, -2, -5, 12, 17, 6, 10, -25,
|
||||
-26, -4, -4, -10, 3, 3, 33, -12,
|
||||
-35, -1, -20, -23, -15, 24, 38, -22,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
]
|
||||
|
||||
PAWN_ENDGAME_SCORES: array[64, Score] = [
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
178, 173, 158, 134, 147, 132, 165, 187,
|
||||
94, 100, 85, 67, 56, 53, 82, 84,
|
||||
32, 24, 13, 5, -2, 4, 17, 17,
|
||||
13, 9, -3, -7, -7, -8, 3, -1,
|
||||
4, 7, -6, 1, 0, -5, -1, -8,
|
||||
13, 8, 8, 10, 13, 0, 2, -7,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
]
|
||||
|
||||
KNIGHT_MIDDLEGAME_SCORES: array[64, Score] = [
|
||||
-167, -89, -34, -49, 61, -97, -15, -107,
|
||||
-73, -41, 72, 36, 23, 62, 7, -17,
|
||||
-47, 60, 37, 65, 84, 129, 73, 44,
|
||||
-9, 17, 19, 53, 37, 69, 18, 22,
|
||||
-13, 4, 16, 13, 28, 19, 21, -8,
|
||||
-23, -9, 12, 10, 19, 17, 25, -16,
|
||||
-29, -53, -12, -3, -1, 18, -14, -19,
|
||||
-105, -21, -58, -33, -17, -28, -19, -23,
|
||||
]
|
||||
|
||||
KNIGHT_ENDGAME_SCORES: array[64, Score] = [
|
||||
-58, -38, -13, -28, -31, -27, -63, -99,
|
||||
-25, -8, -25, -2, -9, -25, -24, -52,
|
||||
-24, -20, 10, 9, -1, -9, -19, -41,
|
||||
-17, 3, 22, 22, 22, 11, 8, -18,
|
||||
-18, -6, 16, 25, 16, 17, 4, -18,
|
||||
-23, -3, -1, 15, 10, -3, -20, -22,
|
||||
-42, -20, -10, -5, -2, -20, -23, -44,
|
||||
-29, -51, -23, -15, -22, -18, -50, -64,
|
||||
]
|
||||
|
||||
BISHOP_MIDDLEGAME_SCORES: array[64, Score] = [
|
||||
-29, 4, -82, -37, -25, -42, 7, -8,
|
||||
-26, 16, -18, -13, 30, 59, 18, -47,
|
||||
-16, 37, 43, 40, 35, 50, 37, -2,
|
||||
-4, 5, 19, 50, 37, 37, 7, -2,
|
||||
-6, 13, 13, 26, 34, 12, 10, 4,
|
||||
0, 15, 15, 15, 14, 27, 18, 10,
|
||||
4, 15, 16, 0, 7, 21, 33, 1,
|
||||
-33, -3, -14, -21, -13, -12, -39, -21,
|
||||
]
|
||||
|
||||
BISHOP_ENDGAME_SCORES: array[64, Score] = [
|
||||
-14, -21, -11, -8, -7, -9, -17, -24,
|
||||
-8, -4, 7, -12, -3, -13, -4, -14,
|
||||
2, -8, 0, -1, -2, 6, 0, 4,
|
||||
-3, 9, 12, 9, 14, 10, 3, 2,
|
||||
-6, 3, 13, 19, 7, 10, -3, -9,
|
||||
-12, -3, 8, 10, 13, 3, -7, -15,
|
||||
-14, -18, -7, -1, 4, -9, -15, -27,
|
||||
-23, -9, -23, -5, -9, -16, -5, -17,
|
||||
]
|
||||
|
||||
ROOK_MIDDLEGAME_SCORES: array[64, Score] = [
|
||||
32, 42, 32, 51, 63, 9, 31, 43,
|
||||
27, 32, 58, 62, 80, 67, 26, 44,
|
||||
-5, 19, 26, 36, 17, 45, 61, 16,
|
||||
-24, -11, 7, 26, 24, 35, -8, -20,
|
||||
-36, -26, -12, -1, 9, -7, 6, -23,
|
||||
-45, -25, -16, -17, 3, 0, -5, -33,
|
||||
-44, -16, -20, -9, -1, 11, -6, -71,
|
||||
-19, -13, 1, 17, 16, 7, -37, -26,
|
||||
]
|
||||
|
||||
ROOK_ENDGAME_SCORES: array[64, Score] = [
|
||||
13, 10, 18, 15, 12, 12, 8, 5,
|
||||
11, 13, 13, 11, -3, 3, 8, 3,
|
||||
7, 7, 7, 5, 4, -3, -5, -3,
|
||||
4, 3, 13, 1, 2, 1, -1, 2,
|
||||
3, 5, 8, 4, -5, -6, -8, -11,
|
||||
-4, 0, -5, -1, -7, -12, -8, -16,
|
||||
-6, -6, 0, 2, -9, -9, -11, -3,
|
||||
-9, 2, 3, -1, -5, -13, 4, -20,
|
||||
]
|
||||
|
||||
QUEEN_MIDDLEGAME_SCORES: array[64, Score] = [
|
||||
-28, 0, 29, 12, 59, 44, 43, 45,
|
||||
-24, -39, -5, 1, -16, 57, 28, 54,
|
||||
-13, -17, 7, 8, 29, 56, 47, 57,
|
||||
-27, -27, -16, -16, -1, 17, -2, 1,
|
||||
-9, -26, -9, -10, -2, -4, 3, -3,
|
||||
-14, 2, -11, -2, -5, 2, 14, 5,
|
||||
-35, -8, 11, 2, 8, 15, -3, 1,
|
||||
-1, -18, -9, 10, -15, -25, -31, -50,
|
||||
]
|
||||
|
||||
QUEEN_ENDGAME_SCORES: array[64, Score] = [
|
||||
-9, 22, 22, 27, 27, 19, 10, 20,
|
||||
-17, 20, 32, 41, 58, 25, 30, 0,
|
||||
-20, 6, 9, 49, 47, 35, 19, 9,
|
||||
3, 22, 24, 45, 57, 40, 57, 36,
|
||||
-18, 28, 19, 47, 31, 34, 39, 23,
|
||||
-16, -27, 15, 6, 9, 17, 10, 5,
|
||||
-22, -23, -30, -16, -16, -23, -36, -32,
|
||||
-33, -28, -22, -43, -5, -32, -20, -41,
|
||||
]
|
||||
|
||||
KING_MIDDLEGAME_SCORES: array[64, Score] = [
|
||||
-65, 23, 16, -15, -56, -34, 2, 13,
|
||||
29, -1, -20, -7, -8, -4, -38, -29,
|
||||
-9, 24, 2, -16, -20, 6, 22, -22,
|
||||
-17, -20, -12, -27, -30, -25, -14, -36,
|
||||
-49, -1, -27, -39, -46, -44, -33, -51,
|
||||
-14, -14, -22, -46, -44, -30, -15, -27,
|
||||
1, 7, -8, -64, -43, -16, 9, 8,
|
||||
-15, 36, 12, -54, 8, -28, 24, 14,
|
||||
]
|
||||
|
||||
KING_ENDGAME_SCORES: array[64, Score] = [
|
||||
-74, -35, -18, -18, -11, 15, 4, -17,
|
||||
-12, 17, 14, 17, 17, 38, 23, 11,
|
||||
10, 17, 23, 15, 20, 45, 44, 13,
|
||||
-8, 22, 24, 27, 26, 33, 26, 3,
|
||||
-18, -4, 21, 24, 27, 23, 9, -11,
|
||||
-19, -3, 11, 21, 23, 16, 7, -9,
|
||||
-27, -11, 4, 13, 14, 4, -5, -17,
|
||||
-53, -34, -21, -11, -28, -14, -24, -43
|
||||
]
|
||||
|
||||
MIDDLEGAME_TABLES: array[6, array[64, Score]] = [
|
||||
BISHOP_MIDDLEGAME_SCORES,
|
||||
KING_MIDDLEGAME_SCORES,
|
||||
KNIGHT_MIDDLEGAME_SCORES,
|
||||
PAWN_MIDDLEGAME_SCORES,
|
||||
QUEEN_MIDDLEGAME_SCORES,
|
||||
ROOK_MIDDLEGAME_SCORES
|
||||
]
|
||||
|
||||
ENDGAME_TABLES: array[6, array[64, Score]] = [
|
||||
BISHOP_ENDGAME_SCORES,
|
||||
KING_ENDGAME_SCORES,
|
||||
KNIGHT_ENDGAME_SCORES,
|
||||
PAWN_ENDGAME_SCORES,
|
||||
QUEEN_ENDGAME_SCORES,
|
||||
ROOK_ENDGAME_SCORES
|
||||
]
|
||||
|
||||
|
||||
|
||||
proc getPieceValue(kind: PieceKind): Score =
|
||||
## Returns the absolute value of a piece
|
||||
case kind:
|
||||
of Pawn:
|
||||
return Score(100)
|
||||
of Bishop:
|
||||
return Score(330)
|
||||
of Knight:
|
||||
return Score(280)
|
||||
of Rook:
|
||||
return Score(525)
|
||||
of Queen:
|
||||
return Score(950)
|
||||
else:
|
||||
discard
|
||||
|
||||
|
||||
proc getPieceScore*(board: Chessboard, square: Square): Score =
|
||||
## Returns the value of the piece located at
|
||||
## the given square
|
||||
return board.getPiece(square).kind.getPieceValue()
|
||||
|
||||
|
||||
proc getEstimatedMoveScore*(board: Chessboard, move: Move): Score =
|
||||
## Returns an estimated static score for the move
|
||||
result = Score(0)
|
||||
if move.isCapture():
|
||||
# Implementation of MVVLVA: Most Valuable Victim Least Valuable Attacker
|
||||
# We prioritize moves that capture the most valuable pieces, and as a
|
||||
# second goal we want to use our least valuable pieces to do so (this
|
||||
# is why we multiply the piece of the captured score by 100, to give
|
||||
# it priority)
|
||||
result = 100 * board.getPieceScore(move.targetSquare) -
|
||||
board.getPieceScore(move.startSquare)
|
||||
|
||||
|
||||
proc getGamePhase(board: Chessboard): int =
|
||||
## Computes the game phase according to
|
||||
## how many pieces are left on the board
|
||||
result = 0
|
||||
for sq in board.getOccupancy():
|
||||
case board.getPiece(sq).kind:
|
||||
of Bishop, Knight:
|
||||
inc(result)
|
||||
of Queen:
|
||||
inc(result, 4)
|
||||
of Rook:
|
||||
inc(result, 2)
|
||||
else:
|
||||
discard
|
||||
# Caps the value in case of early
|
||||
# promotions
|
||||
result = min(24, result)
|
||||
|
||||
|
||||
proc evaluatePiecePositions(board: ChessBoard): Score =
|
||||
## Returns the evaluation of the current
|
||||
## material's position relative to white
|
||||
let
|
||||
middleGamePhase = board.getGamePhase()
|
||||
endGamePhase = 24 - middleGamePhase
|
||||
var
|
||||
# White, Black
|
||||
middleGameScores: array[2, Score] = [0, 0]
|
||||
endGameScores: array[2, Score] = [0, 0]
|
||||
|
||||
for sq in board.getOccupancy():
|
||||
let piece = board.getPiece(sq)
|
||||
middleGameScores[piece.color.int] += MIDDLEGAME_TABLES[piece.kind.int][sq.int]
|
||||
endGameScores[piece.color.int] += ENDGAME_TABLES[piece.kind.int][sq.int]
|
||||
|
||||
let
|
||||
sideToMove = board.position.sideToMove
|
||||
nonSideToMove = sideToMove.opposite()
|
||||
middleGameScore = middleGameScores[sideToMove.int] - middleGameScores[nonSideToMove.int]
|
||||
endGameScore = endGameScores[sideToMove.int] - endGameScores[nonSideToMove.int]
|
||||
|
||||
result = Score((middleGameScore * middleGamePhase + endGameScore * endGamePhase) div 24)
|
||||
|
||||
|
||||
proc evaluateMaterial(board: ChessBoard): Score =
|
||||
## Returns the material evaluation of the
|
||||
## current position relative to white (positive
|
||||
## if in white's favor, negative otherwise)
|
||||
var
|
||||
whiteScore: Score
|
||||
blackScore: Score
|
||||
|
||||
for sq in board.getOccupancyFor(White):
|
||||
whiteScore += board.getPieceScore(sq)
|
||||
|
||||
for sq in board.getOccupancyFor(Black):
|
||||
blackScore += board.getPieceScore(sq)
|
||||
|
||||
result = whiteScore - blackScore
|
||||
if board.position.sideToMove == Black:
|
||||
result *= -1
|
||||
|
||||
|
||||
proc evaluate*(board: Chessboard): Score =
|
||||
## Evaluates the current position
|
||||
|
||||
result = board.evaluateMaterial()
|
||||
result += board.evaluatePiecePositions()
|
|
@ -285,7 +285,7 @@ proc generateMoves*(self: Chessboard, moves: var MoveList) =
|
|||
# King is in double check: no need to generate any more
|
||||
# moves
|
||||
return
|
||||
|
||||
|
||||
self.generateCastling(moves)
|
||||
|
||||
# We pass a mask to our move generators to remove stuff
|
||||
|
|
|
@ -40,11 +40,12 @@ type
|
|||
targetSquare*: Square
|
||||
flags*: uint16
|
||||
|
||||
MoveList* = object
|
||||
MoveList* = ref object
|
||||
## A list of moves
|
||||
data: array[218, Move]
|
||||
data*: array[218, Move]
|
||||
len: int8
|
||||
|
||||
|
||||
func `[]`*(self: MoveList, i: SomeInteger): Move =
|
||||
when not defined(danger):
|
||||
if i >= self.len:
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
# 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.
|
||||
|
||||
## Implementation of negamax with a/b pruning
|
||||
import board
|
||||
import movegen
|
||||
import eval
|
||||
|
||||
|
||||
import std/times
|
||||
import std/atomics
|
||||
import std/algorithm
|
||||
import std/monotimes
|
||||
import std/strformat
|
||||
|
||||
|
||||
func lowestEval*: Score {.inline.} = Score(int32.low() + 1_000_000)
|
||||
func highestEval*: Score {.inline.} = Score(int32.high() - 1_000_000)
|
||||
func mateScore*: Score {.inline.} = lowestEval()
|
||||
|
||||
|
||||
type
|
||||
SearchManager* = ref object
|
||||
## A simple state storage
|
||||
## for our search
|
||||
stopFlag*: Atomic[bool] # Can be used to cancel the search from another thread
|
||||
board: Chessboard
|
||||
bestMoveRoot: Move
|
||||
searchStart: MonoTime
|
||||
searchDeadline: MonoTime
|
||||
nodeCount: uint64
|
||||
maxNodes: uint64
|
||||
searchMoves: seq[Move]
|
||||
|
||||
|
||||
proc newSearchManager*(board: Chessboard): SearchManager =
|
||||
new(result)
|
||||
result.board = board
|
||||
result.bestMoveRoot = nullMove()
|
||||
result.searchMoves = @[]
|
||||
|
||||
|
||||
proc reorderMoves(self: SearchManager, moves: var MoveList) =
|
||||
## Reorders the list of moves in-place, trying
|
||||
## to place the best ones first
|
||||
|
||||
proc orderer(a, b: Move): int {.closure.} =
|
||||
return cmp(self.board.getEstimatedMoveScore(a), self.board.getEstimatedMoveScore(b))
|
||||
|
||||
moves.data.sort(orderer, SortOrder.Descending)
|
||||
|
||||
|
||||
proc timedOut(self: SearchManager): bool = getMonoTime() >= self.searchDeadline
|
||||
proc cancelled(self: SearchManager): bool = self.stopFlag.load()
|
||||
|
||||
|
||||
proc log(self: SearchManager, depth: int) =
|
||||
let
|
||||
elapsed = getMonoTime() - self.searchStart
|
||||
elapsedMsec = elapsed.inMilliseconds.uint64
|
||||
nps = 1000 * (self.nodeCount div max(elapsedMsec, 1))
|
||||
var logMsg = &"info depth {depth} time {elapsedMsec} nodes {self.nodeCount} nps {nps}"
|
||||
if self.bestMoveRoot != nullMove():
|
||||
logMsg &= &" pv {self.bestMoveRoot.toAlgebraic()}"
|
||||
echo logMsg
|
||||
|
||||
|
||||
proc shouldStop(self: SearchManager): bool =
|
||||
## Returns whether searching should
|
||||
## stop
|
||||
if self.cancelled():
|
||||
# Search has been cancelled!
|
||||
return true
|
||||
if self.timedOut():
|
||||
# We ran out of time!
|
||||
return true
|
||||
if self.maxNodes > 0 and self.nodeCount >= self.maxNodes:
|
||||
# Ran out of nodes
|
||||
return true
|
||||
|
||||
|
||||
proc search*(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {.discardable.} =
|
||||
## Simple negamax search with alpha-beta pruning
|
||||
if self.shouldStop():
|
||||
return
|
||||
if depth == 0:
|
||||
return self.board.evaluate()
|
||||
var moves = MoveList()
|
||||
self.board.generateMoves(moves)
|
||||
self.reorderMoves(moves)
|
||||
if moves.len() == 0:
|
||||
if self.board.inCheck():
|
||||
# Checkmate! We add the current ply
|
||||
# because mating in 3 is better than
|
||||
# mating in 5 (and conversely being
|
||||
# mated in 5 is better than being
|
||||
# mated in 3)
|
||||
return mateScore() + Score(ply)
|
||||
# Stalemate
|
||||
return Score(0)
|
||||
var bestScore = lowestEval()
|
||||
var alpha = alpha
|
||||
for i, move in moves:
|
||||
if ply == 0 and self.searchMoves.len() > 0 and move notin self.searchMoves:
|
||||
continue
|
||||
self.board.makeMove(move)
|
||||
inc(self.nodeCount)
|
||||
# Find the best move for us (worst move
|
||||
# for our opponent, hence the negative sign)
|
||||
let eval = -self.search(depth - 1, ply + 1, -beta, -alpha)
|
||||
self.board.unmakeMove()
|
||||
# When a search is cancelled or times out, we need
|
||||
# to make sure the entire call stack unwindss back
|
||||
# to the root move. This is why the check is duplicated
|
||||
if self.shouldStop():
|
||||
return
|
||||
bestScore = max(eval, bestScore)
|
||||
if eval >= beta:
|
||||
# This move was too good for us, opponent will not search it
|
||||
break
|
||||
if eval > alpha:
|
||||
alpha = eval
|
||||
if ply == 0:
|
||||
self.bestMoveRoot = move
|
||||
return bestScore
|
||||
|
||||
|
||||
proc findBestMove*(self: SearchManager, maxSearchTime, maxDepth: int, maxNodes: uint64, searchMoves: seq[Move]): Move =
|
||||
## Finds the best move in the current position
|
||||
## and returns it, limiting search time to
|
||||
## maxSearchTime milliseconds and to maxDepth
|
||||
## ply (if maxDepth is -1, a reasonable limit
|
||||
## is picked). If maxNodes is supplied and is nonzero,
|
||||
## search will stop once it has analyzed the given number
|
||||
## of nodes. If searchMoves is provided and is not empty,
|
||||
## search will be restricted to the moves in the list
|
||||
self.bestMoveRoot = nullMove()
|
||||
result = self.bestMoveRoot
|
||||
self.maxNodes = maxNodes
|
||||
self.searchMoves = searchMoves
|
||||
self.searchStart = getMonoTime()
|
||||
self.searchDeadline = self.searchStart + initDuration(milliseconds=maxSearchTime)
|
||||
var maxDepth = maxDepth
|
||||
if maxDepth == -1:
|
||||
maxDepth = 30
|
||||
# Iterative deepening loop
|
||||
for i in 1..maxDepth:
|
||||
self.search(i, 0, lowestEval(), highestEval())
|
||||
self.log(i)
|
||||
if self.shouldStop():
|
||||
break
|
||||
result = self.bestMoveRoot
|
|
@ -15,28 +15,30 @@
|
|||
## Implementation of a UCI compatible server
|
||||
import std/strutils
|
||||
import std/strformat
|
||||
import std/random
|
||||
|
||||
randomize()
|
||||
import std/atomics
|
||||
|
||||
|
||||
import board
|
||||
import movegen
|
||||
import search
|
||||
|
||||
|
||||
type
|
||||
UCISession = ref object
|
||||
debug: bool
|
||||
board: Chessboard
|
||||
searching: bool
|
||||
currentSearch: SearchManager
|
||||
|
||||
UCICommandType = enum
|
||||
Unknown
|
||||
IsReady
|
||||
NewGame
|
||||
Unknown,
|
||||
IsReady,
|
||||
NewGame,
|
||||
Quit,
|
||||
Debug,
|
||||
Position,
|
||||
Go
|
||||
Go,
|
||||
Stop
|
||||
|
||||
UCICommand = object
|
||||
case kind: UCICommandType
|
||||
|
@ -55,44 +57,13 @@ type
|
|||
movesToGo: int
|
||||
depth: int
|
||||
moveTime: int
|
||||
nodes: uint64
|
||||
searchmoves: seq[Move]
|
||||
else:
|
||||
discard
|
||||
|
||||
|
||||
proc handleUCIGoCommand(session: UCISession, command: seq[string]): UCICommand =
|
||||
result = UCICommand(kind: Go)
|
||||
result.wtime = -1
|
||||
result.btime = -1
|
||||
result.winc = 0
|
||||
result.binc = 0
|
||||
result.movesToGo = 0
|
||||
result.depth = 0
|
||||
result.moveTime = -1
|
||||
var
|
||||
current = 0
|
||||
while current < command.len():
|
||||
case command[current]:
|
||||
of "infinite":
|
||||
discard
|
||||
of "wtime":
|
||||
result.wtime = command[current + 1].parseInt()
|
||||
of "btime":
|
||||
result.btime = command[current + 1].parseInt()
|
||||
of "winc":
|
||||
result.winc = command[current + 1].parseInt()
|
||||
of "binc":
|
||||
result.binc = command[current + 1].parseInt()
|
||||
of "result.movestogo":
|
||||
result.movesToGo = command[current + 1].parseInt()
|
||||
of "depth":
|
||||
result.depth = command[current + 1].parseInt()
|
||||
of "movetime":
|
||||
result.moveTime = command[current + 1].parseInt()
|
||||
else:
|
||||
return
|
||||
|
||||
|
||||
proc handleUCIMove(session: UCISession, move: string): tuple[move: Move, cmd: UCICommand] {.discardable.} =
|
||||
proc parseUCIMove(session: UCISession, move: string): tuple[move: Move, command: UCICommand] =
|
||||
var
|
||||
startSquare: Square
|
||||
targetSquare: Square
|
||||
|
@ -136,10 +107,68 @@ proc handleUCIMove(session: UCISession, move: string): tuple[move: Move, cmd: UC
|
|||
flags.add(Castle)
|
||||
elif targetSquare == session.board.position.enPassantSquare:
|
||||
flags.add(EnPassant)
|
||||
let move = createMove(startSquare, targetSquare, flags)
|
||||
result.move = createMove(startSquare, targetSquare, flags)
|
||||
|
||||
|
||||
proc handleUCIMove(session: UCISession, move: string): tuple[move: Move, cmd: UCICommand] {.discardable.} =
|
||||
if session.debug:
|
||||
echo &"info string making move {move}"
|
||||
result.move = session.board.makeMove(move)
|
||||
let
|
||||
r = session.parseUCIMove(move)
|
||||
move = r.move
|
||||
command = r.command
|
||||
if move == nullMove():
|
||||
return (move, command)
|
||||
else:
|
||||
result.move = session.board.makeMove(move)
|
||||
|
||||
|
||||
proc handleUCIGoCommand(session: UCISession, command: seq[string]): UCICommand =
|
||||
result = UCICommand(kind: Go)
|
||||
result.wtime = 0
|
||||
result.btime = 0
|
||||
result.winc = 0
|
||||
result.binc = 0
|
||||
result.movesToGo = 0
|
||||
result.depth = -1
|
||||
result.moveTime = -1
|
||||
result.nodes = 0
|
||||
var
|
||||
current = 1 # Skip the "go"
|
||||
while current < command.len():
|
||||
let flag = command[current]
|
||||
inc(current)
|
||||
case flag:
|
||||
of "infinite":
|
||||
result.wtime = int32.high()
|
||||
result.btime = int32.high()
|
||||
of "wtime":
|
||||
result.wtime = command[current].parseInt()
|
||||
of "btime":
|
||||
result.btime = command[current].parseInt()
|
||||
of "winc":
|
||||
result.winc = command[current].parseInt()
|
||||
of "binc":
|
||||
result.binc = command[current].parseInt()
|
||||
of "movestogo":
|
||||
result.movesToGo = command[current].parseInt()
|
||||
of "depth":
|
||||
result.depth = command[current].parseInt()
|
||||
of "movetime":
|
||||
result.moveTime = command[current].parseInt()
|
||||
of "nodes":
|
||||
result.nodes = command[current].parseBiggestUInt()
|
||||
of "searchmoves":
|
||||
while current < command.len():
|
||||
inc(current)
|
||||
if command[current] == "":
|
||||
break
|
||||
let move = session.parseUCIMove(command[current]).move
|
||||
if move == nullMove():
|
||||
return UCICommand(kind: Unknown, reason: &"invalid move '{command[current]}' for searchmoves")
|
||||
result.searchmoves.add(move)
|
||||
else:
|
||||
discard
|
||||
|
||||
|
||||
proc handleUCIPositionCommand(session: UCISession, command: seq[string]): UCICommand =
|
||||
|
@ -213,6 +242,8 @@ proc parseUCICommand(session: UCISession, command: string): UCICommand =
|
|||
case cmd[current]:
|
||||
of "isready":
|
||||
return UCICommand(kind: IsReady)
|
||||
of "stop":
|
||||
return UCICommand(kind: Stop)
|
||||
of "ucinewgame":
|
||||
return UCICommand(kind: NewGame)
|
||||
of "quit":
|
||||
|
@ -237,6 +268,37 @@ proc parseUCICommand(session: UCISession, command: string): UCICommand =
|
|||
inc(current)
|
||||
|
||||
|
||||
proc bestMove(args: tuple[session: UCISession, command: UCICommand]) {.thread.} =
|
||||
## Finds the best move in the current position
|
||||
{.cast(gcsafe).}:
|
||||
setControlCHook(proc () {.noconv.} = quit(0))
|
||||
# Yes yes nim sure this isn't gcsafe. Now stfu and spawn a thread
|
||||
var session = args.session
|
||||
var command = args.command
|
||||
session.searching = true
|
||||
session.currentSearch = newSearchManager(session.board)
|
||||
session.currentSearch.stopFlag.store(false)
|
||||
var
|
||||
timeRemaining = (if session.board.position.sideToMove == White: command.wtime else: command.btime)
|
||||
increment = (if session.board.position.sideToMove == White: command.winc else: command.binc)
|
||||
maxTime = (timeRemaining div 20) + (increment div 2)
|
||||
# Buffer to avoid loosing on time
|
||||
if maxTime == 0:
|
||||
maxTime = int32.high()
|
||||
else:
|
||||
maxTime -= 100
|
||||
if command.moveTime != -1:
|
||||
maxTime = command.moveTime
|
||||
if session.debug:
|
||||
echo &"info string starting search to depth {command.depth} for at most {maxTime} ms and {command.nodes} nodes"
|
||||
if session.debug and command.searchmoves.len() > 0:
|
||||
echo &"""info string restricting search to: {command.searchmoves.join(" ")}"""
|
||||
var move = session.currentSearch.findBestMove(maxTime, command.depth, command.nodes, command.searchmoves)
|
||||
session.searching = false
|
||||
echo &"bestmove {move.toAlgebraic()}"
|
||||
|
||||
|
||||
|
||||
proc startUCISession* =
|
||||
## Begins listening for UCI commands
|
||||
echo "id name Nimfish 0.1"
|
||||
|
@ -270,12 +332,11 @@ proc startUCISession* =
|
|||
of NewGame:
|
||||
session.board = newDefaultChessboard()
|
||||
of Go:
|
||||
var moves = MoveList()
|
||||
session.board.generateMoves(moves)
|
||||
if session.debug:
|
||||
echo &"info string generated {len(moves)} moves"
|
||||
if moves.len() > 0:
|
||||
echo &"bestmove {moves[rand(0..<moves.len())].toAlgebraic()}"
|
||||
var thread: Thread[tuple[session: UCISession, command: UCICommand]]
|
||||
createThread(thread, bestMove, (session, cmd))
|
||||
of Stop:
|
||||
if session.searching:
|
||||
session.currentSearch.stopFlag.store(true)
|
||||
of Position:
|
||||
discard
|
||||
else:
|
||||
|
|
Loading…
Reference in New Issue