-
Notifications
You must be signed in to change notification settings - Fork 14
/
game.go
298 lines (250 loc) · 8.15 KB
/
game.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
// Copyright (c) 2014-2018 by Michael Dvorkin. All Rights Reserved.
// Use of this source code is governed by a MIT-style license that can
// be found in the LICENSE file.
//
// I am making my contributions/submissions to this project solely in my
// personal capacity and am not conveying any rights to any intellectual
// property of any third parties.
package donna
import (
`fmt`
`strings`
`time`
)
type RootPv struct {
size int
moves [MaxPly]Move
}
type Pv [MaxPly]RootPv
type History [14][64]int
type Killers [MaxPly][2]Move
type Game struct {
nodes int // Number of regular nodes searched.
qnodes int // Number of quiescence nodes searched.
token uint8 // Cache's expiration token.
deepening bool // True when searching first root move.
improving bool // True when root search score is not falling.
volatility float32 // Root search stability count.
initial string // Initial position (FEN or algebraic).
history History // Good moves history.
killers Killers // Killer moves.
rootpv RootPv // Principal variation for root moves.
pv Pv // Principal variations for each ply.
cache Cache // Transposition table.
pawnCache PawnCache // Cache of pawn structures.
}
// Use single statically allocated variable.
var game Game
// We have two ways to initialize the game: 1) pass FEN string, and 2) specify
// white and black pieces using regular chess notation.
//
// In latter case we need to tell who gets to move first when starting the game.
// The second option is a bit less pricise (ex. no en-passant square) but it is
// much more useful when writing tests from memory.
func NewGame(args ...string) *Game {
game = Game{ cache: NewCache(engine.cacheSize), pawnCache: PawnCache{} }
switch len(args) {
case 0: // Initial position.
game.initial = `rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1`
case 1: // Genuine FEN.
game.initial = args[0]
case 2: // Donna chess format (white and black).
game.initial = args[0] + ` : ` + args[1]
}
return &game
}
func (game *Game) start() *Position {
engine.clock.halt = false
tree, node, rootNode = [1024]Position{}, 0, 0
// Was the game started with FEN or algebraic notation?
sides := strings.Split(game.initial, ` : `)
if len(sides) == 2 {
return NewPosition(game, sides[White], sides[Black])
}
return NewPositionFromFEN(game, game.initial)
}
func (game *Game) position() *Position {
return &tree[node]
}
// Resets principal variation as well as killer moves and move history. Cache
// entries get expired by incrementing cache token. Root node gets set to the
// current tree node to match the position.
func (game *Game) getReady() *Game {
game.rootpv = RootPv{}
game.pv = Pv{}
game.killers = Killers{}
game.history = History{}
game.deepening = false
game.improving = true
game.volatility = 0.0
game.token += 4 // <-- Wraps around: ...248, 252, 0, 4... reserving last 2 bits.
rootNode = node
return game
}
// Copies the very latest top principal variation line.
func updateRootPv() {
if game.pv[0].size > 0 {
copy(game.rootpv.moves[0:], game.pv[0].moves[0:])
game.rootpv.size = game.pv[0].size
}
}
// "The question of whether machines can think is about as relevant as the
// question of whether submarines can swim." -- Edsger W. Dijkstra
func (game *Game) Think() Move {
start := time.Now()
position := game.position()
game.nodes, game.qnodes = 0, 0
if len(engine.bookFile) != 0 {
if book, err := NewBook(engine.bookFile); err == nil {
if move := book.pickMove(position); move != 0 {
game.printBestMove(move, since(start))
return move
}
} else if !engine.uci {
fmt.Printf("Book error: %v\n", err)
}
}
game.getReady()
score, move, status, alpha, beta := 0, Move(0), InProgress, -Checkmate, Checkmate
if !engine.uci {
fmt.Println(ansiWhite + `Depth Time Nodes QNodes Nodes/s Cache Score Best` + ansiNone)
}
if !engine.fixedDepth() {
engine.startClock(); defer engine.stopClock();
}
for depth := 1; game.keepThinking(depth, status, move); depth++ {
// Save previous best score in case search gets interrupted.
bestScore := score
// Assume volatility decreases with each new iteration.
game.volatility /= 2.0
// At low depths do the search with full alpha/beta spread.
// Aspiration window searches kick in at depth 5 and up.
if depth < 5 {
score = position.search(alpha, beta, depth)
if score > alpha || depth == 1 {
bestScore = score
updateRootPv()
}
} else {
aspiration := onePawn / 3
alpha = max(score - aspiration, -Checkmate)
beta = min(score + aspiration, Checkmate)
// Do the search with smaller alpha/beta spread based on
// previous iteration score, and re-search with the bigger
// window as necessary.
for {
score = position.search(alpha, beta, depth)
if score > alpha {
bestScore = score
updateRootPv()
}
if !engine.fixedDepth() && engine.clock.halt {
break
}
if score <= alpha {
game.improving = false
alpha = max(score - aspiration, -Checkmate)
} else if score >= beta {
beta = min(score + aspiration, Checkmate)
} else {
break;
}
aspiration *= 2
}
// TBD: position.cache(game.rootpv[0], score, 0, 0)
}
if engine.clock.halt {
score = bestScore
}
move = game.rootpv.moves[0]
status = position.status(move, score)
game.printPrincipal(depth, score, status, since(start))
}
game.printBestMove(move, since(start))
return move
}
// When in doubt, do what the President does ―- guess.
func (game *Game) keepThinking(depth, status int, move Move) bool {
if depth == 1 || depth > MaxDepth || status != InProgress {
return depth == 1
}
if engine.fixedDepth() {
return depth <= engine.options.maxDepth
} else if engine.clock.halt {
return false
}
// Stop deepening if it's the only move.
gen := NewRootGen(nil, depth)
if gen.onlyMove() {
//\\ engine.debug("# Depth %02d Only move %s\n", depth, move)
return false
}
// Stop if the time left is not enough to gets through the next iteration.
if engine.varyingTime() {
elapsed := engine.elapsed(time.Now())
remaining := engine.factor(depth, game.volatility).remaining()
//\\ engine.debug("# Depth %02d Volatility %.2f Elapsed %s Remaining %s\n", depth, game.volatility, ms(elapsed), ms(remaining))
if elapsed > remaining {
//\\ engine.debug("# Depth %02d Bailing out with %s\n", depth, move)
return false
}
}
return true
}
func (game *Game) printBestMove(move Move, duration int64) {
if engine.uci {
engine.uciBestMove(move, duration)
} else {
engine.replBestMove(move)
}
}
// Prints principal variation. Note that in REPL advantage white is always +score
// and advantage black is -score whereas in UCI +score is advantage current side
// and -score is advantage opponent.
func (game *Game) printPrincipal(depth, score, status int, duration int64) {
if engine.uci {
engine.uciPrincipal(depth, score, duration)
} else {
if game.position().color == Black {
score = -score
}
engine.replPrincipal(depth, score, status, duration)
}
}
func (game *Game) saveBest(ply int, move Move) *Game {
game.pv[ply].moves[ply] = move
game.pv[ply].size = ply + 1
next := game.pv[ply].size
if size := game.pv[next].size; next < MaxPly && size > next {
copy(game.pv[ply].moves[next:], game.pv[next].moves[next:size])
game.pv[ply].size += size - next
}
return game
}
func (game *Game) saveGood(depth int, move Move) *Game {
if move.isQuiet() {
if ply := ply(); move != game.killers[ply][0] {
game.killers[ply][1] = game.killers[ply][0]
game.killers[ply][0] = move
}
game.history[move.piece()][move.to()] += depth * depth
}
return game
}
func (game *Game) updatePoor(depth int, bestMove Move, mgen *MoveGen) *Game {
value := depth * depth
for move := mgen.nextMove(); move != 0; move = mgen.nextMove() {
if move.isQuiet() {
game.history[move.piece()][move.to()] = let(move == bestMove, value, -value)
}
}
return game
}
// Checks whether the move is among good moves captured so far and returns its
// history value.
func (game *Game) good(move Move) int {
return game.history[move.piece()][move.to()]
}
func (game *Game) String() string {
return game.position().String()
}