forked from skybrian/Gongo
-
Notifications
You must be signed in to change notification settings - Fork 1
/
gtp.go
413 lines (346 loc) · 8.96 KB
/
gtp.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
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
package gongo
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
"regexp"
"sort"
"strconv"
"strings"
)
// Engine-specific constants used in the Go Text Protocol
const GTPProtocolVersion = "2"
const GTPEngineName = "Gongo"
const GTPEngineVersion = "0.1.0"
// The gongo package handles I/O for Go-playing robots written in Go.
// A Go robot is normally implemented as a command-line tool that
// takes commands from a controller on stdin and writes responses to
// stdout. The Go Text Protocol [1] defines how this should be done.
// A robot that implements GTP can be plugged into various useful tools,
// such as GoGui [2], which provides a user interface.
//
// (The latest version of the GTP spec is a draft; apparently this was
// never finalized, but hasn't changed in a while.)
//
// [1] http://www.lysator.liu.se/~gunnar/gtp/gtp2-spec-draft2/gtp2-spec.html
// [2] http://gogui.sourceforge.net/
// === public API ===
// Executes GTP commands using the specified robot.
// Returns nil after the "quit" command is handled,
// or non nil for an I/O error (which could be EOF).
func Run(robot GoRobot, input io.Reader, out io.Writer) error {
in := bufio.NewReader(input)
for {
command, args, err := parseCommand(in)
if err != nil {
return err
}
next_handler, ok := handlers[command]
if !ok {
fmt.Fprint(out, error_("unknown command"))
continue
}
fmt.Fprint(out, next_handler(request{robot, args}))
if command == "quit" {
break
}
}
return nil
}
// GTP protocol doesn't support larger than 25x25
const MaxBoardSize = 25
type GoBoard interface {
// debug support (for showboard)
GetBoardSize() int
GetCell(x, y int) Color
// Adds a move to the board. Moves can be added for either side in any
// order, for example to set up a position. If the same player plays twice,
// it's assumed that the other player passed. The board automatically
// handle captures.
// The x and y coordinates start at 1, where x goes from left to right
// and y from bottom to top. Playing at (0,0) means pass.
// Returns:
// ok - true if the move was accepted or false for an illegal move
// message - status or error message, for debugging. May be empty.
Play(c Color, x, y int) (ok bool, message string)
}
type GoRobot interface {
// Attempts to change the board size. If the robot doesn't support the
// new size, return false. (In any case, board sizes above 25 aren't
// supported by GTP.)
// The controller should call ClearBoard next, or the results are undefined.
SetBoardSize(size int) (ok bool)
ClearBoard()
SetKomi(komi float64)
// Asks the robot to generate a move at the current position for the given
// color. The robot may be asked to play a move for either side.
// The result is one of Played, Passed, or Resigned.
GenMove(color Color) (x, y int, result MoveResult)
Debug() string
GoBoard
}
// === types used by the GoRobot interface ===
type Color int
const (
Empty = Color(0)
Black = Color(1)
White = Color(2)
)
func ParseColor(input string) (c Color, ok bool) {
switch strings.ToLower(input) {
case "w", "white":
return White, true
case "b", "black":
return Black, true
}
return Empty, false
}
func (c Color) GetOpponent() Color {
switch c {
case Black:
return White
case White:
return Black
}
panic(fmt.Sprintf("can't get opponent for %v", c))
}
func (c Color) String() string {
switch c {
case White:
return "White"
case Black:
return "Black"
case Empty:
return "Empty"
}
panic("invalid color")
}
type MoveResult int
const (
Played MoveResult = 0
Passed MoveResult = 1
Resigned MoveResult = 2
)
func (m MoveResult) String() string {
switch m {
case Played:
return "Played"
case Passed:
return "Passed"
case Resigned:
return "Resigned"
}
panic("invalid move result")
}
// === driver implementation ===
var word_regexp = regexp.MustCompile("[^ ]+")
func parseCommand(in *bufio.Reader) (cmd string, args []string, err error) {
for {
line, err := in.ReadString('\n')
if err != nil {
return "", nil, err
}
line = strings.TrimSpace(line)
if line != "" && line[0] != '#' {
words := word_regexp.FindAllString(line, -1)
return words[0], words[1:], nil
}
}
return "", nil, errors.New("shouldn't get here")
}
type handler func(request) response
type request struct {
robot GoRobot
args []string
}
type response struct {
message string
success bool
}
func success(message string) response { return response{message, true} }
func error_(message string) response { return response{message, false} }
func (r response) String() string {
prefix := "="
if !r.success {
prefix = "?"
}
return prefix + " " + r.message + "\n\n"
}
var handlers map[string]handler
func init() {
_known := func(req request) response { return handle_known_command(req) }
_list := func(req request) response { return handle_list_commands(req) }
handlers = map[string]handler{
"boardsize": handle_boardsize,
"clear_board": func(req request) response {
req.robot.ClearBoard()
return success("")
},
"genmove": handle_genmove,
"known_command": _known,
"komi": handle_komi,
"list_commands": _list,
"name": func(req request) response { return success(GTPEngineName) },
"play": handle_play,
"protocol_version": func(req request) response { return success(GTPProtocolVersion) },
"quit": func(req request) response { return success("") },
"showboard": handle_showboard,
"debug": handle_debug,
"version": func(req request) response { return success(GTPEngineVersion) },
}
}
func handle_known_command(req request) response {
if len(req.args) != 1 {
return error_("wrong number of arguments")
}
_, ok := handlers[req.args[0]]
return success(fmt.Sprint(ok))
}
func handle_list_commands(req request) response {
if len(req.args) != 0 {
return error_("wrong number of arguments")
}
names := make([]string, len(handlers))
i := 0
for name := range handlers {
names[i] = name
i++
}
sort.Strings(names)
return success(strings.Join(names, "\n"))
}
func handle_boardsize(req request) response {
if len(req.args) != 1 {
return error_("wrong number of arguments")
}
size, err := strconv.Atoi(req.args[0])
if err != nil {
return error_("unacceptable size")
}
if !req.robot.SetBoardSize(size) {
return error_("unacceptable size")
}
return success("")
}
func handle_komi(req request) response {
if len(req.args) != 1 {
return error_("wrong number of arguments")
}
komi, err := strconv.ParseFloat(req.args[0], 64)
if err != nil {
return error_("syntax error")
}
req.robot.SetKomi(komi)
return success("")
}
func handle_play(req request) response {
if len(req.args) != 2 {
return error_("wrong number of arguments")
}
color, ok := ParseColor(req.args[0])
if !ok {
return error_("syntax error")
}
x, y, ok := stringToVertex(req.args[1])
if !ok {
return error_("syntax error")
}
ok, detail := req.robot.Play(color, x, y)
if !ok {
fmt.Fprintf(os.Stderr, "Illegal move: %s\n", detail)
return error_("illegal move")
}
return success("")
}
func handle_genmove(req request) (response response) {
if len(req.args) != 1 {
return error_("wrong number of arguments")
}
color, ok := ParseColor(req.args[0])
if !ok {
return error_("syntax error")
}
x, y, status := req.robot.GenMove(color)
switch status {
case Played:
message, ok := vertexToString(x, y)
if ok {
response = success(message)
} else {
response = error_(message)
}
case Passed:
response = success("pass")
case Resigned:
response = success("resign")
}
return
}
func handle_showboard(req request) response {
if len(req.args) != 0 {
return error_("wrong number of arguments")
}
size := req.robot.GetBoardSize()
buf := &bytes.Buffer{}
for y := size; y >= 1; y-- {
for x := 1; x <= size; x++ {
color := req.robot.GetCell(x, y)
switch color {
case Empty:
buf.WriteString(".")
case White:
buf.WriteString("O")
case Black:
buf.WriteString("@")
default:
panic("shouldn't happen")
}
}
if y > 1 {
buf.WriteString("\n")
}
}
return success(buf.String())
}
func stringToVertex(input string) (x, y int, ok bool) {
input = strings.ToUpper(input)
if len(input) < 2 {
return 0, 0, false
}
if input == "PASS" {
return 0, 0, true
}
x = 1 + int(input[0]) - int('A')
if input[0] > 'I' {
x--
}
if x < 1 || x > MaxBoardSize {
return 0, 0, false
}
y, err := strconv.Atoi(input[1:len(input)])
if err != nil || y < 1 || y > MaxBoardSize {
return 0, 0, false
}
return x, y, true
}
func vertexToString(x, y int) (result string, ok bool) {
// FIXME should this handle passes too ?
if x < 1 || x > MaxBoardSize || y < 1 || y > MaxBoardSize {
return fmt.Sprintf("invalid: (%v,%v)", x, y), false
}
x_letter := byte(x) - 1 + 'A'
if x_letter >= 'I' {
x_letter++
}
return fmt.Sprintf("%c%v", x_letter, y), true
}
func handle_debug(req request) response {
if len(req.args) != 0 {
return error_("wrong number of arguments")
}
dbginfo := req.robot.Debug()
return success(dbginfo)
}