Initial work on search and eval
This commit is contained in:
parent
ce960003a2
commit
629718a54c
|
@ -0,0 +1,69 @@
|
|||
# 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* = int16
|
||||
|
||||
|
||||
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 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()
|
|
@ -0,0 +1,74 @@
|
|||
# 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/atomics
|
||||
|
||||
|
||||
func lowestEval*: Score {.inline.} = Score(-32000'i16)
|
||||
func highestEval*: Score {.inline.} = Score(32000'i16)
|
||||
func mateScore*: Score {.inline.} = lowestEval() - Score(1)
|
||||
|
||||
|
||||
type
|
||||
SearchManager* = ref object
|
||||
## A simple state storage
|
||||
## for our search
|
||||
stopFlag*: Atomic[bool] # Can be used to cancel the search from another thread
|
||||
bestMove*: Move
|
||||
|
||||
|
||||
proc search*(self: SearchManager, board: Chessboard, depth, ply: int, alpha, beta: Score): Score {.discardable.} =
|
||||
## Simple negamax search with alpha-beta pruning
|
||||
if self.stopFlag.load():
|
||||
# Search has been cancelled!
|
||||
return
|
||||
if depth == 0:
|
||||
return board.evaluate()
|
||||
var moves = MoveList()
|
||||
board.generateMoves(moves)
|
||||
if moves.len() == 0:
|
||||
if 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 move in moves:
|
||||
board.makeMove(move)
|
||||
# Find the best move for us (worst move
|
||||
# for our opponent, hence the negative sign)
|
||||
let eval = -self.search(board, depth - 1, ply + 1, -beta, -alpha)
|
||||
board.unmakeMove()
|
||||
|
||||
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.bestMove = move
|
||||
|
||||
return bestScore
|
|
@ -17,26 +17,31 @@ import std/strutils
|
|||
import std/strformat
|
||||
import std/random
|
||||
|
||||
|
||||
randomize()
|
||||
|
||||
|
||||
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
|
||||
|
@ -213,6 +218,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 +244,16 @@ proc parseUCICommand(session: UCISession, command: string): UCICommand =
|
|||
inc(current)
|
||||
|
||||
|
||||
proc bestMove(session: UCISession, command: UCICommand) =
|
||||
## Finds the best move in the current position
|
||||
session.searching = true
|
||||
session.currentSearch = SearchManager()
|
||||
session.currentSearch.search(session.board, 6, 0, lowestEval(), highestEval())
|
||||
session.searching = false
|
||||
let move = session.currentSearch.bestMove
|
||||
echo &"bestmove {move.toAlgebraic()}"
|
||||
|
||||
|
||||
proc startUCISession* =
|
||||
## Begins listening for UCI commands
|
||||
echo "id name Nimfish 0.1"
|
||||
|
@ -270,12 +287,7 @@ 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()}"
|
||||
session.bestMove(cmd)
|
||||
of Position:
|
||||
discard
|
||||
else:
|
||||
|
|
Loading…
Reference in New Issue