-
Notifications
You must be signed in to change notification settings - Fork 0
/
search.go
334 lines (295 loc) · 9.86 KB
/
search.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
package main
import (
"context"
"errors"
"fmt"
"sort"
)
var (
errCheckmate checkmateError
errStalemate = errors.New("stalemate")
errFiftyMove = errors.New("fifty-move rule")
// TODO: threefold repetition
// openWindow encompasses all possible Rels.
openWindow = Window{
Rel{err: errCheckmate}, // worst case: mated
Rel{err: errCheckmate.Prev()}, // best case: mate in 1 ply
}
)
// Search contains parameters relevant to a position search.
type Search struct {
// allowCutoff describes whether to allow alpha-beta pruning.
allowCutoff bool
// counters tracks the number of nodes searched at each depth.
counters []int
}
// SearchPosition searches a Position to the specified depth via iterative deepening and returns the search results.
func SearchPosition(ctx context.Context, pos Position, depth int) Results {
var rs Results
s := Search{
allowCutoff: true,
counters: make([]int, depth+1),
}
for d := 1; d <= depth; d++ {
_, rs = s.negamax(ctx, pos, rs, openWindow, d)
if ctx.Err() != nil {
break
}
}
return rs
}
// negamax recursively searches a Position to the specified depth and returns the evaluation score
// relative to the side to move and the search results. It employs alpha-beta pruning outside of
// the specified Window. If rs is zero length, negamax will generate and search all legal
// moves; if recommended moves are provided, they must all be legal, and only they will be searched.
func (s *Search) negamax(
ctx context.Context,
pos Position,
rs Results,
w Window,
depth int,
) (bestScore Rel, results Results) {
s.counters[len(s.counters)-1-depth]++
err := checkTerminal(pos)
if err != nil && (s.allowCutoff || err != errInsufficient) {
// Do not cut off during perft in the case of insufficient material
return Rel{err: err}, nil
}
if len(rs) == 0 {
rs = legalResults(pos)
}
// Invariant: len(rs) > 0 and rs contains only legal moves
if w.beta.err == errCheckmate {
// An alternative move from this position's parent delivers mate; no need to search this one.
return rs[0].score.Rel(pos.ToMove), rs
}
if depth == 0 {
score := Eval(pos)
return score.Rel(pos.ToMove), rs
}
if s.allowCutoff && deepEnough(rs, depth) {
return rs[0].score.Rel(pos.ToMove), rs
}
if w.alpha.err == errCheckmate {
// There is at least one legal move, so the worst case is not checkmate.
w = Window{Rel{err: errCheckmate.Prev().Prev()}, w.beta}
}
for _, r := range rs {
if r.score.err != nil && s.allowCutoff {
// The move is already known not to avoid a game-ending state; no need to search it further.
continue
}
score, cont := s.negamax(ctx, Make(pos, r.move), r.cont, w.Next(), depth-1)
score = score.Prev()
rs.Update(Result{move: r.move, score: score.Abs(pos.ToMove), depth: depth - 1, cont: cont})
if depth >= 3 && ctx.Err() != nil {
break
}
if !s.allowCutoff {
continue
}
var ok bool
w, ok = w.Constrain(score)
if !ok {
// beta cutoff
break
}
}
rs.SortFor(pos.ToMove)
return w.alpha, rs
}
// checkTerminal returns an error describing the type of terminal position represented by pos, or nil if pos is not terminal.
func checkTerminal(pos Position) error {
if IsMate(pos) {
if IsCheck(pos) {
return errCheckmate
}
return errStalemate
}
if pos.HalfMove >= 100 {
// fifty-move rule
return errFiftyMove
}
return nil
}
// legalResults returns a Results containing all legal moves in pos.
func legalResults(pos Position) Results {
moves := LegalMoves(pos)
rs := make(Results, 0, len(moves))
for _, m := range moves {
rs = append(rs, Result{move: m})
}
return rs
}
// deepEnough reports whether rs stores the results of a position search to at least the specified depth.
func deepEnough(rs Results, depth int) bool {
// rs is already deep enough if all non-terminal elements have been searched to at least depth-1,
// as long as they have been searched to non-zero depth.
for _, r := range rs {
if (r.depth == 0 || r.depth < depth-1) && r.score.err == nil {
return false
}
}
return true
}
// IsPseudoLegal returns whether a Move is pseudo-legal in a Position.
// A move is pseudo-legal if the square to be moved from contains the specified piece
// and the piece is capable of moving to the target square if doing so would not put the king in check.
func IsPseudoLegal(pos Position, move Move) bool {
for _, m := range PseudoLegalMoves(pos) {
if m == move {
return true
}
}
return false
}
// IsLegal returns whether a Position is legal.
// A position is illegal if the king of the side that just moved is in check.
func IsLegal(pos Position) bool {
return !IsAttacked(pos, pos.KingSquare[pos.Opp()], pos.ToMove)
}
// IsCheck returns whether the king of the side to move is in check.
func IsCheck(pos Position) bool {
return IsAttacked(pos, pos.KingSquare[pos.ToMove], pos.Opp())
}
// IsMate returns whether or not a Position is checkmate or stalemate.
// A position is checkmate or stalemate if the side to move has no legal moves.
func IsMate(pos Position) bool { return !hasLegalMove(pos) }
// Result holds a searched Move along with its evaluated score,
// the search depth, and the continuation Results of the search.
type Result struct {
move Move
score Abs
depth int
cont Results
}
// String returns a string representation of r, including its principal variation.
func (r Result) String() string {
s := fmt.Sprintf("%v (%v) %v", r.score, r.depth, r.PV())
if r.score.err != nil {
return s + " " + r.score.err.Error()
}
return s
}
// PV returns a string representation of r's principal variation.
func (r Result) PV() string {
if r.depth == 0 || len(r.cont) == 0 {
return LongAlgebraic(r.move)
}
return LongAlgebraic(r.move) + " " + r.cont[0].PV()
}
// Results contains the results of a search. Functions returning a Results should sort it before returning.
type Results []Result
// SortFor sorts rs beginning with the best move for c.
// The sort is not guaranteed to be stable.
func (rs Results) SortFor(c Color) {
// Sort first by terminal condition, then by depth decreasing,
// then by Score decreasing/increasing for White/Black,
// and then by origin and destination Square increasing.
sort.Slice(rs, func(i, j int) bool {
less := Less(Score(rs[i].score), Score(rs[j].score))
if ierr, jerr := rs[i].score.err, rs[j].score.err; ierr != nil || jerr != nil {
_, ich := ierr.(checkmateError)
_, jch := jerr.(checkmateError)
if ich || jch {
return !less
}
return !less == (c == White)
}
if rs[i].depth != rs[j].depth {
return rs[i].depth > rs[j].depth
}
if rs[i].score != rs[j].score {
return !less == (c == White)
}
return rs.squareSort(i, j)
})
}
// SortBySquares sorts rs first by the Result moves' origin Squares and then by their destination Squares,
// and finally by promotion piece in the case of pawn promotions.
func (rs Results) SortBySquares() { sort.Slice(rs, rs.squareSort) }
// squareSort reports how two Result elements should be sorted by their moves' origin and destination Squares,
// and then finally by promotion piece in the case of promoting pawns.
func (rs Results) squareSort(i, j int) bool {
if rs[i].move.From != rs[j].move.From {
return rs[i].move.From < rs[j].move.From
}
if rs[i].move.To != rs[j].move.To {
return rs[i].move.To < rs[j].move.To
}
// From and To are non-unique in the case of pawn promotion
return rs[i].move.PromotePiece < rs[j].move.PromotePiece
}
// Update finds the Result in rs with the same Move as r and replaces it with r.
// It panics if rs does not contain any elements with r's Move.
func (rs Results) Update(r Result) {
for i := range rs {
if rs[i].move == r.move {
rs[i] = r
return
}
}
// rs should contain all legal moves
panic("unreached")
}
// String returns a string representation of all Result values in r.
func (rs Results) String() string {
var s string
for _, r := range rs {
s += fmt.Sprintf("%v\n", r.String())
}
return s
}
// checkmateError indicates the number of plies until a forced checkmate can be delivered.
// Odd values are wins for the player with the next move, and even values for the player with the previous move.
// The zero value of type checkmateError indicates that the current position is checkmate.
type checkmateError int
func (n checkmateError) Error() string {
if n&1 == 0 {
return fmt.Sprintf("-#%d", n/2)
}
return fmt.Sprintf("#%d", (n+1)/2)
}
// Prev returns the checkmateError corresponding to n's previous ply.
func (n checkmateError) Prev() checkmateError { return n + 1 }
// Next returns the checkmateError corresponding to n's following ply.
// It panics if n is not positive.
func (n checkmateError) Next() checkmateError {
if n < 1 {
panic(fmt.Sprintf("non-positive checkmateError %v", n))
}
return n - 1
}
// Prev returns the Rel corresponding to s's previous ply.
func (s Rel) Prev() Rel {
if err, ok := s.err.(checkmateError); ok {
return Rel{-s.n, err.Prev()}
}
return Rel{-s.n, s.err}
}
// Next returns the Rel corresponding to s's following ply.
func (s Rel) Next() Rel {
if err, ok := s.err.(checkmateError); ok {
return Rel{-s.n, err.Next()}
}
return Rel{-s.n, s.err}
}
// Window represents the bounds of a position's evaluation.
type Window struct{ alpha, beta Rel }
// Constrain updates the lower bound of w, if applicable, and returns the updated Window
// and a boolean value reporting whether the returned Window remains valid.
// Constrain employs fail-hard beta cutoff. Invariant: alpha <= beta in the returned Window.
func (w Window) Constrain(s Rel) (c Window, ok bool) {
switch {
case Less(Score(s), Score(w.alpha)):
return w, true
case Less(Score(w.beta), Score(s)):
return Window{w.beta, w.beta}, false
default:
return Window{s, w.beta}, true
}
}
// Next returns the Window corresponding to w's following ply.
func (w Window) Next() Window { return Window{w.beta.Next(), w.alpha.Next()} }
// String returns a string representation of w.
func (w Window) String() string { return fmt.Sprintf("(%v, %v)", w.alpha, w.beta) }