From e01d17b148f6148a98beb72d739a9100ca4bf83e Mon Sep 17 00:00:00 2001 From: Jeremy Hertel Date: Thu, 30 Nov 2023 23:12:52 -0600 Subject: [PATCH] search+rosaline: move iterative deepening into NegamaxSearcher and keep track of principal variation --- cmd/rosaline/interfaces/cli.go | 12 ++-- cmd/rosaline/interfaces/uci.go | 22 +------ internal/search/negamax.go | 109 +++++++++++++++++++------------- internal/search/negamax_test.go | 2 +- 4 files changed, 73 insertions(+), 72 deletions(-) diff --git a/cmd/rosaline/interfaces/cli.go b/cmd/rosaline/interfaces/cli.go index ed0588a..2152bc0 100644 --- a/cmd/rosaline/interfaces/cli.go +++ b/cmd/rosaline/interfaces/cli.go @@ -83,17 +83,15 @@ func (i cliInterface) Loop() { } } - results := i.searcher.Search(&position, depth) - fmt.Println("best move:", results.BestMove) - fmt.Println("score:", results.Score) - fmt.Println("elapsed:", results.Time.Seconds()) + bestMove := i.searcher.Search(&position, depth, false) + fmt.Println("best move:", bestMove) } else if cmd == "evaluate" { score := i.evaluator.Evaluate(&position) fmt.Println("score:", score) } else if cmd == "play" { - results := i.searcher.Search(&position, DefaultDepth) - position.MakeMove(results.BestMove) - fmt.Println("played:", results.BestMove) + bestMove := i.searcher.Search(&position, DefaultDepth, false) + position.MakeMove(bestMove) + fmt.Println("played:", bestMove) } else if cmd == "fen" { fmt.Println(position.Fen()) } else if cmd == "setfen" { diff --git a/cmd/rosaline/interfaces/uci.go b/cmd/rosaline/interfaces/uci.go index ac6a062..079e670 100644 --- a/cmd/rosaline/interfaces/uci.go +++ b/cmd/rosaline/interfaces/uci.go @@ -3,7 +3,6 @@ package interfaces import ( "bufio" "fmt" - "math" "os" "rosaline/internal/chess" "rosaline/internal/evaluation" @@ -53,28 +52,9 @@ loop: break case "go": go func() { - var bestMove chess.Move - var bestScore int = math.MinInt - - for depth := 1; depth <= 4; depth++ { - results := i.searcher.Search(&position, depth) - - if i.searcher.Stopped() { - break - } - - if results.Score > bestScore { - bestMove = results.BestMove - bestScore = results.Score - } - - fmt.Printf("info depth %d score cp %d nodes %d nps %f time %d tbhits %d\n", depth, results.Score, results.Nodes, results.NPS, results.Time.Milliseconds(), results.Hits) - } - + bestMove := i.searcher.Search(&position, DefaultDepth, true) position.MakeMove(bestMove) fmt.Println("bestmove", bestMove) - - i.searcher.ClearPreviousSearch() }() break case "stop": diff --git a/internal/search/negamax.go b/internal/search/negamax.go index b90671e..04680c4 100644 --- a/internal/search/negamax.go +++ b/internal/search/negamax.go @@ -2,10 +2,12 @@ package search import ( "cmp" + "fmt" "math" "rosaline/internal/chess" "rosaline/internal/evaluation" "slices" + "strings" "time" ) @@ -16,18 +18,11 @@ const ( maxNumberKillerMoves = 128 nullMovePruningReduction = 2 -) -type SearchResults struct { - BestMove chess.Move - Score int - Depth int - Nodes int - Time time.Duration - NPS float64 - Hits int - Misses int -} + window = 500 + + MaxDepth = 16 +) type NegamaxSearcher struct { evaluator evaluation.Evaluator @@ -38,6 +33,9 @@ type NegamaxSearcher struct { ttable TranspositionTable + pvtable [MaxDepth][MaxDepth]chess.Move + pvlength [MaxDepth]int + stop bool nodes int @@ -54,31 +52,27 @@ func NewNegamaxSearcher(evaluator evaluation.Evaluator) NegamaxSearcher { } } -func (s *NegamaxSearcher) Search(position *chess.Position, depth int) SearchResults { - s.nodes = 0 - s.stop = false - - s.ttable.ResetCounters() - - start := time.Now() +func (s *NegamaxSearcher) Search(position *chess.Position, depth int, print bool) chess.Move { + s.ClearPreviousSearch() - bestMove := chess.Move{} + bestMove := chess.NullMove bestScore := math.MinInt + 1 - moves := position.GenerateMoves(chess.LegalMoveGeneration) + for d := 1; d <= depth; d++ { + start := time.Now() - slices.SortFunc(moves, func(m1, m2 chess.Move) int { - return cmp.Compare(s.scoreMove(position, m1), s.scoreMove(position, m2)) - }) + score := s.doSearch(position, initialAlpha, initialBeta, d, 0, 0) - for _, move := range moves { - position.MakeMove(move) - score := -s.doSearch(position, initialAlpha, initialBeta, depth, 0, 0) - position.Undo() + elapsed := time.Since(start) if score > bestScore { bestScore = score - bestMove = move + bestMove = s.pvtable[0][0] + } + + if print { + nps := float64(s.nodes) / float64(elapsed.Milliseconds()) + fmt.Printf("info depth %d score cp %d nodes %d nps %f pv %s time %d tbhits %d\n", d, bestScore, s.nodes, nps, s.getPV(), elapsed.Milliseconds(), s.ttable.Hits()) } if s.stop { @@ -86,23 +80,31 @@ func (s *NegamaxSearcher) Search(position *chess.Position, depth int) SearchResu } } - elapsed := time.Since(start) - nps := float64(s.nodes) / float64(elapsed.Milliseconds()) - - return SearchResults{ - BestMove: bestMove, - Score: bestScore, - Depth: depth, - Nodes: s.nodes, - Time: elapsed, - NPS: nps, - Hits: s.ttable.hits, - Misses: s.ttable.misses, + return bestMove +} + +func (s NegamaxSearcher) getPV() string { + var builder strings.Builder + + length := s.pvlength[0] + for i := 0; i < length; i++ { + builder.WriteString(s.pvtable[0][i].String()) + + if i != length { + builder.WriteString(" ") + } } + + return builder.String() } -func (s NegamaxSearcher) scoreMove(position *chess.Position, move chess.Move) int { +func (s NegamaxSearcher) scoreMove(position *chess.Position, move chess.Move, ply int) int { turn := position.Turn() + + if s.pvtable[0][ply] == move { + return 2000 + } + if slices.Contains(s.killerMoves[turn], move) { return 1000 } @@ -111,6 +113,8 @@ func (s NegamaxSearcher) scoreMove(position *chess.Position, move chess.Move) in } func (s *NegamaxSearcher) doSearch(position *chess.Position, alpha int, beta int, depth int, ply int, extensions int) int { + s.pvlength[ply] = ply + if s.stop { return 0 } @@ -124,8 +128,8 @@ func (s *NegamaxSearcher) doSearch(position *chess.Position, alpha int, beta int } pvNode := beta-alpha != 1 - inCheck := position.IsKingInCheck(position.Turn()) + if inCheck && extensions < 2 { // limit the number of depth increases we will do to 2 depth++ extensions++ @@ -155,6 +159,8 @@ func (s *NegamaxSearcher) doSearch(position *chess.Position, alpha int, beta int if entry.Depth >= depth { switch entry.Type { case ExactNode: + s.pvlength[ply] = ply + 1 + s.pvtable[ply][ply] = entry.Move return entry.Score case UpperNode: if entry.Score < alpha { @@ -189,7 +195,7 @@ func (s *NegamaxSearcher) doSearch(position *chess.Position, alpha int, beta int } slices.SortFunc(moves, func(m1, m2 chess.Move) int { - return cmp.Compare(s.scoreMove(position, m1), s.scoreMove(position, m2)) + return cmp.Compare(s.scoreMove(position, m1, ply), s.scoreMove(position, m2, ply)) }) bestMove := chess.NullMove @@ -238,6 +244,15 @@ func (s *NegamaxSearcher) doSearch(position *chess.Position, alpha int, beta int if score > alpha { alpha = score nodeType = ExactNode + + s.pvtable[ply][ply] = move + + for i := ply + 1; i < s.pvlength[ply+1]; i++ { + pvMove := s.pvtable[ply+1][i] + s.pvtable[ply][i] = pvMove + } + + s.pvlength[ply] = s.pvlength[ply+1] } } @@ -286,8 +301,16 @@ func (s NegamaxSearcher) Stopped() bool { } func (s *NegamaxSearcher) ClearPreviousSearch() { + s.nodes = 0 + s.stop = false + + s.ttable.ResetCounters() + clear(s.killerMoves) s.killerMoveIndex = 0 + + s.pvtable = [MaxDepth][MaxDepth]chess.Move{} + s.pvlength = [MaxDepth]int{} } // Reset clears any information about searched positions. diff --git a/internal/search/negamax_test.go b/internal/search/negamax_test.go index 3bd3619..7eefad7 100644 --- a/internal/search/negamax_test.go +++ b/internal/search/negamax_test.go @@ -12,6 +12,6 @@ func BenchmarkNegamax(b *testing.B) { searcher := NewNegamaxSearcher(evaluator) for i := 0; i < b.N; i++ { - searcher.Search(&position, 4) + searcher.Search(&position, 4, false) } }