-
Notifications
You must be signed in to change notification settings - Fork 14
/
engine.go
292 lines (251 loc) · 8.5 KB
/
engine.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
// 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`; `os`; `time`)
const Ping = 250 // Check time 4 times a second.
type Clock struct {
halt bool // Stop search immediately when set to true.
softStop int64 // Target soft time limit to make a move.
hardStop int64 // Immediate stop time limit.
extra float32 // Extra time factor based on search volatility.
start time.Time
ticker *time.Ticker
}
type Options struct {
ponder bool // (-) Pondering mode.
infinite bool // (-) Search until the "stop" command.
maxDepth int // Search X plies only.
maxNodes int // (-) Search X nodes only.
moveTime int64 // Search exactly X milliseconds per move.
movesToGo int64 // Number of moves to make till time control.
timeLeft int64 // Time left for all remaining moves.
timeInc int64 // Time increment after the move is made.
}
type Engine struct {
log bool // Enable logging.
uci bool // Use UCI protocol.
trace bool // Trace evaluation scores.
fancy bool // Represent pieces as UTF-8 characters.
status uint8 // Engine status.
logFile string // Log file name.
bookFile string // Polyglot opening book file name.
cacheSize float64 // Default cache size.
clock Clock
options Options
}
// Use single statically allocated variable.
var engine Engine
func NewEngine(args ...interface{}) *Engine {
engine = Engine{}
for i := 0; i < len(args); i += 2 {
switch value := args[i+1]; args[i] {
case `log`:
engine.log = value.(bool)
case `logfile`:
engine.logFile = value.(string)
case `bookfile`:
engine.bookFile = value.(string)
case `uci`:
engine.uci = value.(bool)
case `trace`:
engine.trace = value.(bool)
case `fancy`:
engine.fancy = value.(bool)
case `depth`:
engine.options.maxDepth = value.(int)
case `movetime`:
engine.options.moveTime = int64(value.(int))
case `cache`:
switch value.(type) {
default: // :-)
engine.cacheSize = value.(float64)
case int:
engine.cacheSize = float64(value.(int))
}
}
}
return &engine
}
// Dumps the string to standard output.
func (e *Engine) print(arg string) *Engine {
os.Stdout.WriteString(arg)
os.Stdout.Sync() // <-- Flush it.
return e
}
// Appends the string to log file. No flush is required as f.Write() and friends
// are unbuffered.
func (e *Engine) debug(args ...interface{}) *Engine {
if len(e.logFile) != 0 {
logFile, err := os.OpenFile(e.logFile, os.O_CREATE | os.O_WRONLY | os.O_APPEND, 0666)
if err == nil {
defer logFile.Close()
if len := len(args); len > 1 {
logFile.WriteString(fmt.Sprintf(args[0].(string), args[1:]...))
} else {
logFile.WriteString(args[0].(string))
}
}
}
return e
}
// Dumps the string to standard output and optionally logs it to file.
func (e *Engine) reply(args ...interface{}) *Engine {
if len := len(args); len > 1 {
data := fmt.Sprintf(args[0].(string), args[1:]...)
e.print(data)
//\\ e.debug(data)
} else if len == 1 {
e.print(args[0].(string))
//\\ e.debug(args[0].(string))
}
return e
}
func (e *Engine) fixedDepth() bool {
return e.options.maxDepth > 0
}
func (e *Engine) fixedTime() bool {
return e.options.moveTime > 0
}
func (e *Engine) varyingTime() bool {
return e.options.moveTime == 0
}
// Returns elapsed time in milliseconds.
func (e *Engine) elapsed(now time.Time) int64 {
return now.Sub(e.clock.start).Nanoseconds() / 1000000 //int64(time.Millisecond)
}
// Returns remaining search time to make a move. The remaining time extends the
// soft stop estimate based on search volatility factor.
func (e *Engine) remaining() int64 {
return int64(float32(e.clock.softStop) * e.clock.extra)
}
// Sets extra time factor. For depths 5+ we take into account search volatility,
// i.e. extra time is given for uncertain positions where the best move is not clear.
func (e *Engine) factor(depth int, volatility float32) *Engine {
e.clock.extra = 0.75
if depth >= 5 {
e.clock.extra *= (volatility + 1.0)
}
return e
}
// Starts the clock setting ticker callback function. The callback function is
// different for fixed and variable time controls.
func (e *Engine) startClock() *Engine {
e.clock.halt = false
if e.options.moveTime == 0 && e.options.timeLeft == 0 {
return e
}
e.clock.start = time.Now()
e.clock.ticker = time.NewTicker(time.Millisecond * Ping)
if e.fixedTime() {
return e.fixedTimeTicker()
}
// How long a minute is depends on which side of the bathroom door you're on.
return e.varyingTimeTicker()
}
// Stop the clock so that the ticker callback function is longer invoked.
func (e *Engine) stopClock() *Engine {
if e.clock.ticker != nil {
e.clock.ticker.Stop()
e.clock.ticker = nil
}
return e
}
// Ticker callback for fixed time control (ex. 5s per move). Search gets terminated
// when we've got the move and the elapsed time approaches time-per-move limit.
func (e *Engine) fixedTimeTicker() *Engine {
go func() {
if e.clock.ticker == nil {
return // Nothing to do if the clock has been stopped.
}
for now := range e.clock.ticker.C {
if game.rootpv.size == 0 {
continue // Haven't found the move yet.
}
if e.elapsed(now) >= e.options.moveTime - Ping {
e.clock.halt = true
return
}
}
}()
return e
}
// Ticker callback for the variable time control (ex. 40 moves in 5 minutes). Search
// termination depends on multiple factors with hard stop being the ultimate limit.
func (e *Engine) varyingTimeTicker() *Engine {
go func() {
if e.clock.ticker == nil {
return // Nothing to do if the clock has been stopped.
}
for now := range e.clock.ticker.C {
if game.rootpv.size == 0 {
continue // Haven't found the move yet.
}
elapsed := e.elapsed(now)
if (game.deepening && game.improving && elapsed > e.remaining() * 4 / 5) || elapsed > e.clock.hardStop {
//\\ e.debug("# Halt: Flags %v Elapsed %s Remaining %s Hard stop %s\n",
//\\ game.deepening && game.improving, ms(elapsed), ms(e.remaining() * 4 / 5), ms(e.clock.hardStop))
e.clock.halt = true
return
}
}
}()
return e
}
// Sets fixed search limits such as maximum depth or time to make a move.
func (e *Engine) fixedLimit(options Options) *Engine {
e.options = options
return e
}
// Sets variable time control options and calculates soft and hard stop estimates.
func (e *Engine) varyingLimits(options Options) *Engine {
// Note if it's a new time control before saving the options.
e.options = options
e.options.ponder = false
e.options.infinite = false
e.options.maxDepth = 0
e.options.maxNodes = 0
e.options.moveTime = 0
// Set default number of moves till the end of the game or time control.
// TODO: calculate based on game phase.
if e.options.movesToGo == 0 {
e.options.movesToGo = 40
}
// Calculate hard and soft stop estimates.
moves := e.options.movesToGo - 1
hard := options.timeLeft + options.timeInc * moves
soft := hard / e.options.movesToGo
//\\ e.debug("#\n# Make %d moves in %s soft stop %s hard stop %s\n", e.options.movesToGo, ms(e.options.timeLeft), ms(soft), ms(hard))
// Adjust hard stop to leave enough time reserve for the remaining moves. The time
// reserve starts at 100% of soft stop for one remaining move, and goes down to 80%
// in 1% decrement for 20+ remaining moves.
if moves > 0 { // The last move gets all remaining time and doesn't need the reserve.
percent := max64(80, 100 - moves)
reserve := soft * moves * percent / 100
//\\ e.debug("# Reserve %d%% = %s\n", percent, ms(reserve))
if hard - reserve > soft {
hard -= reserve
}
// Hard stop can't exceed optimal time to make 3 moves.
hard = min64(hard, soft * 3)
//\\ e.debug("# Hard stop %s\n", ms(hard))
}
// Set the final values for soft and hard stops making sure the soft stop
// never exceeds the hard one.
if soft < hard {
e.clock.softStop, e.clock.hardStop = soft, hard
} else {
e.clock.softStop, e.clock.hardStop = hard, soft
}
// Keep two ping cycles available to avoid accidental time forefeit.
e.clock.hardStop -= 2 * Ping
if e.clock.hardStop < 0 {
e.clock.hardStop = options.timeLeft // Oh well...
}
//\\ e.debug("# Final soft stop %s hard stop %s\n#\n", ms(e.clock.softStop), ms(e.clock.hardStop))
return e
}