Initial work on search and eval

This commit is contained in:
Mattia Giambirtone 2024-04-24 19:38:54 +02:00
parent ce960003a2
commit 629718a54c
3 changed files with 165 additions and 10 deletions

View File

@ -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()

View File

@ -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

View File

@ -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: