├── .gitignore ├── go.mod ├── go.sum ├── LICENSE ├── README.md └── michi.go /.gitignore: -------------------------------------------------------------------------------- 1 | patterns.* 2 | michi-go* 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/traveller42/michi-go 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/OneOfOne/xxhash v1.2.8 7 | github.com/bszcz/mt19937_64 v0.0.0-20130910145052-1d1d0a529983 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= 2 | github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= 3 | github.com/bszcz/mt19937_64 v0.0.0-20130910145052-1d1d0a529983 h1:CdWJjaOgTh+PNSgXO28y6SVX2fCanyke5jBdZQy67SI= 4 | github.com/bszcz/mt19937_64 v0.0.0-20130910145052-1d1d0a529983/go.mod h1:yQTVB0R60BUs04ANKGiQe96vvXiRh4Y15521jgFW5Vg= 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Clark Wierda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Michi-Go --- Michi recoded in Go 2 | ================================ 3 | 4 | This is a recoding in Go (for speed) of the michi.py code by Petr Baudis avalaible at 5 | https://github.com/pasky/michi. 6 | 7 | Michi stands for "Minimalistic Pachi". This is a Minimalistic Go MCTS Engine. The aims of the project are best explained by the author: 8 | 9 | > Michi aims to be a minimalistic but full-fledged Computer Go program based 10 | > on state-of-art methods (Monte Carlo Tree Search) and written in Python. 11 | > Our goal is to make it easier for new people to enter the domain of 12 | > Computer Go, peek under the hood of a "real" playing engine and be able 13 | > to learn by hassle-free experiments - with the algorithms, add heuristics, 14 | > etc. 15 | 16 | > This is not meant to be a competitive engine; simplicity and clear code is 17 | > preferred over optimization (after all, it's in Python!). But compared to 18 | > other minimalistic engines, this one should be able to beat beginner 19 | > intermediate human players, and I believe that a *fast* implementation 20 | > of exactly the same heuristics would be around 4k KGS or even better. 21 | 22 | Please go on his project page to read more about Michi and to find some information about theory or interesting projects to do. 23 | 24 | Michi-Go is distributed under the MIT licence. Now go forth, hack and peruse! 25 | 26 | (This README adapted from the README at https://github.com/db3108/michi-c) 27 | -------------------------------------------------------------------------------- /michi.go: -------------------------------------------------------------------------------- 1 | // A minimalistic Go-playing engine attempting to strike a balance between 2 | // brevity, educational value and strength. 3 | // Based on michi.py by Petr Baudis (https://github.com/pasky/michi) 4 | package main 5 | 6 | import ( 7 | "bufio" 8 | "bytes" 9 | "fmt" 10 | "math" 11 | "math/rand" 12 | "os" 13 | "regexp" 14 | "runtime" 15 | "strconv" 16 | "strings" 17 | "time" 18 | "unicode" 19 | 20 | xxhash "github.com/OneOfOne/xxhash" 21 | mt64 "github.com/bszcz/mt19937_64" 22 | ) 23 | 24 | // Given a board of size NxN (N=9, 19, ...), we represent the position 25 | // as an (N+1)*(N+2) string, with '.' (empty), 'X' (to-play player), 26 | // 'x' (other player), and whitespace (off-board border to make rules 27 | // implementation easier). Coordinates are just indices in this string. 28 | // You can simply print(board) when debugging. 29 | const ( 30 | N = 13 // boardsize 31 | W = N + 2 // arraysize including buffer for board edge 32 | columnString = "ABCDEFGHJKLMNOPQRST" // labels for columns 33 | MAX_GAME_LEN = N * N * 3 34 | ) 35 | 36 | // emptyBoard is a byte slice representing an empty board 37 | var emptyBoard []byte 38 | 39 | const ( 40 | PASS = -1346458457 // 'P','A','S','S' 0x50415353 41 | NONE = -1313820229 // 'N','O','N','E' 0x4e4f4e45 42 | ) 43 | 44 | // constants related to the operation of the MCTS move selection 45 | const ( 46 | N_SIMS = 1400 // number of playouts for Monte-Carlo Search 47 | RAVE_EQUIV = 3500 48 | EXPAND_VISITS = 8 49 | PRIOR_EVEN = 10 // should be even number; 0.5 prior 50 | PRIOR_SELFATARI = 10 // negative prior 51 | PRIOR_CAPTURE_ONE = 15 52 | PRIOR_CAPTURE_MANY = 30 53 | PRIOR_PAT3 = 10 54 | PRIOR_LARGEPATTERN = 100 // most moves have relatively small probability 55 | PRIOR_EMPTYAREA = 10 56 | REPORT_PERIOD = 200 57 | PROB_SSAREJECT = 0.9 // probability of rejecting suggested self-atari in playout 58 | PROB_RSAREJECT = 0.5 // probability of rejecting random self-atari in playout; this is lower than above to allow nakade 59 | RESIGN_THRES = 0.2 60 | FASTPLAY20_THRES = 0.8 // if at 20% playouts winrate is >this, stop reading 61 | FASTPLAY5_THRES = 0.95 // if at 5% playouts winrate is >this, stop reading 62 | ) 63 | 64 | var PRIOR_CFG = [...]int{24, 22, 8} // priors for moves in cfg dist. 1, 2, 3 65 | 66 | // probability of heuristic suggestions being taken in playout 67 | var PROB_HEURISTIC = map[string]float64{ 68 | "capture": 0.9, 69 | "pat3": 0.95, 70 | } 71 | 72 | // filenames for the large patterns from the Pachi Go engine 73 | const ( 74 | spatPatternDictFile = "patterns.spat" 75 | largePatternsFile = "patterns.prob" 76 | ) 77 | 78 | // 3x3 playout patterns; X,O are colors, x,o are their inverses 79 | var pattern3x3Source = [...][][]byte{ 80 | {{'X', 'O', 'X'}, // hane pattern - enclosing hane 81 | {'.', '.', '.'}, 82 | {'?', '?', '?'}}, 83 | {{'X', 'O', '.'}, // hane pattern - non-cutting hane 84 | {'.', '.', '.'}, 85 | {'?', '.', '?'}}, 86 | {{'X', 'O', '?'}, // hane pattern - magari 87 | {'X', '.', '.'}, 88 | {'x', '.', '?'}}, 89 | // {{'X','O','O'}, // hane pattern - thin hane 90 | // {'.','.','.'}, 91 | // {'?','.','?'}} "X", - only for the X player 92 | {{'.', 'O', '.'}, // generic pattern - katatsuke or diagonal attachment; similar to magari 93 | {'X', '.', '.'}, 94 | {'.', '.', '.'}}, 95 | {{'X', 'O', '?'}, // cut1 pattern (kiri] - unprotected cut 96 | {'O', '.', 'o'}, 97 | {'?', 'o', '?'}}, 98 | {{'X', 'O', '?'}, // cut1 pattern (kiri] - peeped cut 99 | {'O', '.', 'X'}, 100 | {'?', '?', '?'}}, 101 | {{'?', 'X', '?'}, // cut2 pattern (de] 102 | {'O', '.', 'O'}, 103 | {'o', 'o', 'o'}}, 104 | {{'O', 'X', '?'}, // cut keima 105 | {'o', '.', 'O'}, 106 | {'?', '?', '?'}}, 107 | {{'X', '.', '?'}, // side pattern - chase 108 | {'O', '.', '?'}, 109 | {' ', ' ', ' '}}, 110 | {{'O', 'X', '?'}, // side pattern - block side cut 111 | {'X', '.', 'O'}, 112 | {' ', ' ', ' '}}, 113 | {{'?', 'X', '?'}, // side pattern - block side connection 114 | {'x', '.', 'O'}, 115 | {' ', ' ', ' '}}, 116 | {{'?', 'X', 'O'}, // side pattern - sagari 117 | {'x', '.', 'x'}, 118 | {' ', ' ', ' '}}, 119 | {{'?', 'O', 'X'}, // side pattern - cut 120 | {'X', '.', 'O'}, 121 | {' ', ' ', ' '}}, 122 | } 123 | 124 | // 3x3 pattern routines (those patterns stored in pattern3x3Source above) 125 | 126 | // All possible neighborhood configurations matching a given pattern; 127 | // used just for a combinatoric explosion when loading them in an 128 | // in-memory set. 129 | func pat3_expand(pat [][]byte) [][]byte { 130 | pat_rot90 := func(p [][]byte) [][]byte { 131 | return [][]byte{{p[2][0], p[1][0], p[0][0]}, 132 | {p[2][1], p[1][1], p[0][1]}, 133 | {p[2][2], p[1][2], p[0][2]}} 134 | } 135 | pat_vertflip := func(p [][]byte) [][]byte { 136 | return [][]byte{p[2], p[1], p[0]} 137 | } 138 | pat_horizflip := func(p [][]byte) [][]byte { 139 | return [][]byte{{p[0][2], p[0][1], p[0][0]}, 140 | {p[1][2], p[1][1], p[1][0]}, 141 | {p[2][2], p[2][1], p[2][0]}} 142 | } 143 | pat_swapcolors := func(p [][]byte) [][]byte { 144 | l := [][]byte{} 145 | for _, s := range p { 146 | s = bytes.Replace(s, []byte{'X'}, []byte{'Z'}, -1) 147 | s = bytes.Replace(s, []byte{'x'}, []byte{'z'}, -1) 148 | s = bytes.Replace(s, []byte{'O'}, []byte{'X'}, -1) 149 | s = bytes.Replace(s, []byte{'o'}, []byte{'x'}, -1) 150 | s = bytes.Replace(s, []byte{'Z'}, []byte{'O'}, -1) 151 | s = bytes.Replace(s, []byte{'z'}, []byte{'o'}, -1) 152 | l = append(l, s) 153 | } 154 | return l 155 | } 156 | var pat_wildexp func(p []byte, c byte, to []byte) [][]byte 157 | pat_wildexp = func(p []byte, c byte, to []byte) [][]byte { 158 | i := bytes.Index(p, []byte{c}) 159 | if i == -1 { 160 | return append([][]byte{}, p) 161 | } 162 | l := [][]byte{} 163 | for _, t := range bytes.Split(to, []byte{}) { 164 | l = append(l, pat_wildexp(append(append(append([]byte{}, p[:i]...), t[0]), p[i+1:]...), c, to)...) 165 | } 166 | return l 167 | } 168 | pat_wildcards := func(pat []byte) [][]byte { 169 | l := [][]byte{} 170 | for _, p1 := range pat_wildexp(pat, '?', []byte{'.', 'X', 'O', ' '}) { 171 | for _, p2 := range pat_wildexp(p1, 'x', []byte{'.', 'O', ' '}) { 172 | for _, p3 := range pat_wildexp(p2, 'o', []byte{'.', 'X', ' '}) { 173 | l = append(l, p3) 174 | } 175 | } 176 | } 177 | return l 178 | } 179 | 180 | rl := [][]byte{} 181 | for _, p1 := range [][][]byte{pat, pat_rot90(pat)} { 182 | for _, p2 := range [][][]byte{p1, pat_vertflip(p1)} { 183 | for _, p3 := range [][][]byte{p2, pat_horizflip(p2)} { 184 | for _, p4 := range [][][]byte{p3, pat_swapcolors(p3)} { 185 | for _, p5 := range pat_wildcards(bytes.Join(p4, []byte{})) { 186 | rl = append(rl, p5) 187 | } 188 | } 189 | } 190 | } 191 | } 192 | return rl 193 | } 194 | 195 | func pat3set_func() map[string]struct{} { 196 | m := make(map[string]struct{}) 197 | for _, p := range pattern3x3Source { 198 | for _, s := range pat3_expand(p) { 199 | s = bytes.Replace(s, []byte{'O'}, []byte{'x'}, -1) 200 | m[string(s)] = struct{}{} 201 | } 202 | } 203 | return m 204 | } 205 | 206 | var pat3set map[string]struct{} 207 | 208 | var patternGridcularSequence = [][][]int{ // Sequence of coordinate offsets of progressively wider diameters in gridcular metric 209 | {{0, 0}, 210 | {0, 1}, {0, -1}, {1, 0}, {-1, 0}, 211 | {1, 1}, {-1, 1}, {1, -1}, {-1, -1}}, // d=1,2 is not considered separately 212 | {{0, 2}, {0, -2}, {2, 0}, {-2, 0}}, 213 | {{1, 2}, {-1, 2}, {1, -2}, {-1, -2}, {2, 1}, {-2, 1}, {2, -1}, {-2, -1}}, 214 | {{0, 3}, {0, -3}, {2, 2}, {-2, 2}, {2, -2}, {-2, -2}, {3, 0}, {-3, 0}}, 215 | {{1, 3}, {-1, 3}, {1, -3}, {-1, -3}, {3, 1}, {-3, 1}, {3, -1}, {-3, -1}}, 216 | {{0, 4}, {0, -4}, {2, 3}, {-2, 3}, {2, -3}, {-2, -3}, {3, 2}, {-3, 2}, {3, -2}, {-3, -2}, {4, 0}, {-4, 0}}, 217 | {{1, 4}, {-1, 4}, {1, -4}, {-1, -4}, {3, 3}, {-3, 3}, {3, -3}, {-3, -3}, {4, 1}, {-4, 1}, {4, -1}, {-4, -1}}, 218 | {{0, 5}, {0, -5}, {2, 4}, {-2, 4}, {2, -4}, {-2, -4}, {4, 2}, {-4, 2}, {4, -2}, {-4, -2}, {5, 0}, {-5, 0}}, 219 | {{1, 5}, {-1, 5}, {1, -5}, {-1, -5}, {3, 4}, {-3, 4}, {3, -4}, {-3, -4}, {4, 3}, {-4, 3}, {4, -3}, {-4, -3}, {5, 1}, {-5, 1}, {5, -1}, {-5, -1}}, 220 | {{0, 6}, {0, -6}, {2, 5}, {-2, 5}, {2, -5}, {-2, -5}, {4, 4}, {-4, 4}, {4, -4}, {-4, -4}, {5, 2}, {-5, 2}, {5, -2}, {-5, -2}, {6, 0}, {-6, 0}}, 221 | {{1, 6}, {-1, 6}, {1, -6}, {-1, -6}, {3, 5}, {-3, 5}, {3, -5}, {-3, -5}, {5, 3}, {-5, 3}, {5, -3}, {-5, -3}, {6, 1}, {-6, 1}, {6, -1}, {-6, -1}}, 222 | {{0, 7}, {0, -7}, {2, 6}, {-2, 6}, {2, -6}, {-2, -6}, {4, 5}, {-4, 5}, {4, -5}, {-4, -5}, {5, 4}, {-5, 4}, {5, -4}, {-5, -4}, {6, 2}, {-6, 2}, {6, -2}, {-6, -2}, {7, 0}, {-7, 0}}, 223 | } 224 | 225 | //###################### 226 | // Initialization 227 | // collect all dynamic initializations into a callable function 228 | 229 | // performInitialization() collects all the runtime initializations together 230 | // so they can be called from main() allowing better control 231 | func performInitialization() { 232 | emptyBoard = append(append(append(bytes.Repeat([]byte{' '}, N+1), '\n'), 233 | bytes.Repeat(append([]byte{' '}, append(bytes.Repeat([]byte{'.'}, N), '\n')...), N)...), 234 | bytes.Repeat([]byte{' '}, N+2)...) 235 | 236 | pat3set = pat3set_func() 237 | 238 | spatPatternDict = make(map[int]uint64) // hash(neighborhoodGridcular()) <- spatial id 239 | largePatterns = make(map[uint64]float64) // hash(neighborhoodGridcular()) -> probability 240 | } 241 | 242 | //###################### 243 | // board string routines 244 | 245 | // generator of coordinates for all neighbors of c 246 | func neighbors(c int) []int { 247 | return []int{c - 1, c + 1, c - W, c + W} 248 | } 249 | 250 | // generator of coordinates for all diagonal neighbors of c 251 | func diagonalNeighbors(c int) []int { 252 | return []int{c - W - 1, c - W + 1, c + W - 1, c + W + 1} 253 | } 254 | 255 | func boardPut(board []byte, c int, p byte) []byte { 256 | board[c] = p 257 | return board 258 | } 259 | 260 | // replace continuous-color area starting at c with special color # 261 | func floodfill(board []byte, c int) []byte { 262 | localBoard := make([]byte, len(board)) 263 | copy(localBoard, board) 264 | 265 | // XXX: Use bytearray to speed things up? (still needed in golang?) 266 | p := localBoard[c] 267 | localBoard = boardPut(localBoard, c, '#') 268 | fringe := []int{c} 269 | for len(fringe) > 0 { 270 | c, fringe = fringe[len(fringe)-1], fringe[:len(fringe)-1] 271 | for _, d := range neighbors(c) { 272 | if localBoard[d] == p { 273 | localBoard = boardPut(localBoard, d, '#') 274 | fringe = append(fringe, d) 275 | } 276 | } 277 | } 278 | return localBoard 279 | } 280 | 281 | // test if point of color p is adjecent to color # anywhere 282 | // on the board; use in conjunction with floodfill for reachability 283 | func contact(board []byte, p byte) int { 284 | for i := W; i < len(board)-W-1; i++ { 285 | if board[i] == '#' { 286 | for _, j := range neighbors(i) { 287 | if board[j] == p { 288 | return j 289 | } 290 | } 291 | } 292 | } 293 | return NONE 294 | } 295 | 296 | // Use Mersenne Twister as Random Number Generator in place of default 297 | // Use multiple RNG sources to reduce contention 298 | var rng *rand.Rand 299 | 300 | func newRNG() *rand.Rand { 301 | rng := rand.New(mt64.New()) 302 | rng.Seed(time.Now().UnixNano()) 303 | return rng 304 | } 305 | 306 | // mapping function to swap individual characters 307 | func swapCase(r rune) rune { 308 | switch { 309 | case 'a' <= r && r <= 'z': 310 | return r - 'a' + 'A' 311 | case 'A' <= r && r <= 'Z': 312 | return r - 'A' + 'a' 313 | default: 314 | return r 315 | } 316 | } 317 | 318 | // function to apply mapping to string 319 | func SwapCase(str []byte) []byte { 320 | b := make([]byte, len(str)) 321 | for i, c := range str { 322 | r := rune(c) 323 | if unicode.IsUpper(r) { 324 | b[i] = byte(unicode.ToLower(r)) 325 | } else { 326 | b[i] = byte(unicode.ToUpper(r)) 327 | } 328 | } 329 | return b 330 | } 331 | 332 | // shuffle slice elements 333 | func shuffleInt(a []int, rng *rand.Rand) { 334 | for i := len(a) - 1; i > 0; i-- { 335 | j := rng.Intn(i + 1) 336 | a[i], a[j] = a[j], a[i] 337 | } 338 | } 339 | 340 | func shuffleTree(a []*TreeNode, rng *rand.Rand) { 341 | for i := len(a) - 1; i > 0; i-- { 342 | j := rng.Intn(i + 1) 343 | a[i], a[j] = a[j], a[i] 344 | } 345 | } 346 | 347 | // create routine to test existence of element in slice 348 | func intInSlice(intSlice []int, intTest int) bool { 349 | for _, i := range intSlice { 350 | if i == intTest { 351 | return true 352 | } 353 | } 354 | return false 355 | } 356 | 357 | func patternInSet(strSlice map[string]struct{}, strTest []byte) bool { 358 | _, exists := strSlice[string(strTest)] 359 | return exists 360 | } 361 | 362 | // test for edge of board 363 | func isSpace(b byte) bool { 364 | return bytes.Contains([]byte{' ', '\n'}, []byte{b}) 365 | } 366 | 367 | // End of functions added to replace Python functions and methods 368 | 369 | // test if c is inside a single-color diamond and return the diamond 370 | // color or None; this could be an eye, but also a false one 371 | func isEyeish(board []byte, c int) byte { 372 | var eyeColor, otherColor byte 373 | for _, d := range neighbors(c) { 374 | if isSpace(board[d]) { 375 | continue 376 | } 377 | if board[d] == '.' { 378 | return 0 379 | } 380 | if eyeColor == 0 { 381 | eyeColor = board[d] 382 | otherColor = byte(swapCase(rune(eyeColor))) 383 | } else { 384 | if board[d] == otherColor { 385 | return 0 386 | } 387 | } 388 | } 389 | return eyeColor 390 | } 391 | 392 | // test if c is an eye and return its color or None 393 | func isEye(board []byte, c int) byte { 394 | eyeColor := isEyeish(board, c) 395 | if eyeColor == 0 { 396 | return 0 397 | } 398 | 399 | // Eye-like shape, but it could be a falsified eye 400 | falseColor := byte(swapCase(rune(eyeColor))) 401 | falseCount := 0 402 | atEdge := false 403 | for _, d := range diagonalNeighbors(c) { 404 | if isSpace(board[d]) { 405 | atEdge = true 406 | } else { 407 | if board[d] == falseColor { 408 | falseCount += 1 409 | } 410 | } 411 | } 412 | if atEdge { 413 | falseCount += 1 414 | } 415 | if falseCount >= 2 { 416 | return 0 417 | } 418 | return eyeColor 419 | } 420 | 421 | // Implementation of simple Chinese Go rules 422 | 423 | // Position holds the current state of a the game including the stones on the 424 | // board, the captured stones, the current Ko state, the last 2 moves, and the 425 | // komi used. 426 | type Position struct { 427 | board []byte // string representation of board state 428 | cap []int // holds total captured stones for each player 429 | n int // n is how many moves were played so far 430 | ko int // location of prohibited move under simple ko rules 431 | last int // previous move 432 | last2 int // antepenultimate move 433 | komi float64 434 | } 435 | 436 | // play as player X at the given coord c, return the new position 437 | func (p Position) move(c int) (Position, string) { 438 | // Test for ko 439 | if c == p.ko { 440 | return p, "ko" 441 | } 442 | // Are we trying to play in enemy's eye? 443 | inEnemyEye := isEyeish(p.board, c) == 'x' 444 | 445 | board := make([]byte, len(p.board)) 446 | copy(board, p.board) 447 | board = boardPut(board, c, 'X') 448 | // Test for captures, and track ko 449 | capX := p.cap[0] 450 | singleCaps := []int{} 451 | for _, d := range neighbors(c) { 452 | if board[d] != 'x' { 453 | continue 454 | } 455 | // XXX: The following is an extremely naive and SLOW approach 456 | // at things - to do it properly, we should maintain some per-group 457 | // data structures tracking liberties. 458 | fillBoard := floodfill(board, d) // get a board with the adjacent group replaced by '#' 459 | if contact(fillBoard, '.') != NONE { 460 | continue // some liberties left 461 | } 462 | // no liberties left for this group, remove the stones! 463 | captureCount := bytes.Count(fillBoard, []byte{'#'}) 464 | if captureCount == 1 { 465 | singleCaps = append(singleCaps, d) 466 | } 467 | capX += captureCount 468 | board = bytes.Replace(fillBoard, []byte{'#'}, []byte{'.'}, -1) // capture the group 469 | } 470 | // set ko 471 | ko := NONE 472 | if inEnemyEye && len(singleCaps) == 1 { 473 | ko = singleCaps[0] 474 | } 475 | // Test for suicide 476 | if contact(floodfill(board, c), '.') == NONE { 477 | return p, "suicide" 478 | } 479 | 480 | // Update the position and return 481 | p.board = SwapCase(board) 482 | p.cap = []int{p.cap[1], capX} 483 | p.n = p.n + 1 484 | p.ko = ko 485 | p.last2 = p.last // must copy first 486 | p.last = c 487 | p.komi = p.komi 488 | 489 | return p, "ok" 490 | } 491 | 492 | // pass - i.e. return simply a flipped position 493 | func (p Position) passMove() (Position, string) { 494 | p.board = SwapCase(p.board) 495 | p.cap = []int{p.cap[1], p.cap[0]} 496 | p.n = p.n + 1 497 | p.ko = NONE 498 | p.last2 = p.last // must copy first 499 | p.last = PASS 500 | p.komi = p.komi 501 | 502 | return p, "ok" 503 | } 504 | 505 | // Generate a list of moves (includes false positives - suicide moves; 506 | // does not include true-eye-filling moves), starting from a given board 507 | // index (that can be used for randomization) 508 | func (p Position) moves(i0 int, done chan struct{}) chan int { 509 | c := make(chan int) 510 | 511 | go func() { 512 | defer close(c) 513 | 514 | i := i0 - 1 515 | passes := 0 516 | for { 517 | index := bytes.Index(p.board[i+1:], []byte{'.'}) 518 | if passes > 0 && (index == -1 || i+index+1 >= i0) { 519 | return // we have looked through the whole board 520 | } 521 | if index == -1 { 522 | i = 0 523 | passes += 1 524 | continue // go back and start from the beginning 525 | } 526 | i += index + 1 527 | // Test for to-play player's one-point eye 528 | if isEye(p.board, i) == 'X' { 529 | continue 530 | } 531 | select { 532 | case c <- i: 533 | case <-done: 534 | return 535 | } 536 | } 537 | 538 | // close(c) defer'd 539 | }() 540 | return c 541 | } 542 | 543 | // generate a randomly shuffled list of points including and 544 | // surrounding the last two moves (but with the last move having 545 | // priority) 546 | func (p Position) lastMovesNeighbors(rng *rand.Rand) []int { 547 | cList := []int{} 548 | dList := []int{} 549 | for _, c := range []int{p.last, p.last2} { 550 | if c < 0 { // if there was no last move, or pass 551 | continue 552 | } 553 | dList = append([]int{c}, append(neighbors(c), diagonalNeighbors(c)...)...) 554 | shuffleInt(dList, rng) 555 | for _, d := range dList { 556 | if intInSlice(cList, d) { 557 | continue 558 | } 559 | cList = append(cList, d) 560 | } 561 | } 562 | return cList 563 | } 564 | 565 | // compute score for to-play player; this assumes a final position 566 | // with all dead stones captured; if owner_map is passed, it is assumed 567 | // to be an array of statistics with average owner at the end of the game 568 | // (+1 black, -1 white) 569 | func (p Position) score(owner_map []float64) float64 { 570 | board := make([]byte, len(p.board)) 571 | copy(board, p.board) 572 | var fillBoard []byte 573 | var touches_X, touches_x bool 574 | var komi float64 575 | var n float64 576 | i := 0 577 | for { 578 | index := bytes.Index(p.board[i+1:], []byte{'.'}) 579 | if index == -1 { 580 | break 581 | } 582 | i += index + 1 583 | fillBoard = floodfill(board, i) 584 | // fillBoard is board with some continuous area of empty space replaced by # 585 | touches_X = contact(fillBoard, 'X') != NONE 586 | touches_x = contact(fillBoard, 'x') != NONE 587 | if touches_X && !touches_x { 588 | board = bytes.Replace(fillBoard, []byte{'#'}, []byte{'X'}, -1) 589 | } else if touches_x && !touches_X { 590 | board = bytes.Replace(fillBoard, []byte{'#'}, []byte{'x'}, -1) 591 | } else { 592 | board = bytes.Replace(fillBoard, []byte{'#'}, []byte{':'}, -1) // seki, rare 593 | } 594 | } 595 | // now that area is replaced either by X, x or : 596 | if p.n%2 == 1 { 597 | komi = p.komi 598 | } else { 599 | komi = -p.komi 600 | } 601 | if len(owner_map) > 0 { 602 | for c := 0; c < W*W; c++ { 603 | if board[c] == 'X' { 604 | n = 1 605 | } else if board[c] == 'x' { 606 | n = -1 607 | } else { 608 | n = 0 609 | } 610 | if p.n%2 == 1 { 611 | n = -n 612 | } 613 | owner_map[c] += n 614 | } 615 | } 616 | return float64(bytes.Count(board, []byte{'X'})-bytes.Count(board, []byte{'x'})) + komi 617 | } 618 | 619 | // Return an initial board position 620 | func emptyPosition() Position { 621 | var p Position 622 | 623 | p.board = emptyBoard 624 | p.cap = []int{0, 0} 625 | p.n = 0 626 | p.ko = NONE 627 | p.last = NONE 628 | p.last2 = NONE 629 | p.komi = 7.5 630 | 631 | return p 632 | } 633 | 634 | //############## 635 | // go heuristics 636 | 637 | // An atari/capture analysis routine that checks the group at c, 638 | // determining whether (i) it is in atari (ii) if it can escape it, 639 | // either by playing on its liberty or counter-capturing another group. 640 | // N.B. this is maybe the most complicated part of the whole program (sadly); 641 | // feel free to just TREAT IT AS A BLACK-BOX, it's not really that 642 | // interesting! 643 | // The return value is a tuple of (boolean, [coord..]), indicating whether 644 | // the group is in atari and how to escape/capture (or [] if impossible). 645 | // (Note that (False, [...]) is possible in case the group can be captured 646 | // in a ladder - it is not in atari but some capture attack/defense moves 647 | // are available.) 648 | // singlePointOK means that we will not try to save one-point groups; 649 | // twoLibertyTest means that we will check for 2-liberty groups which are 650 | // threatened by a ladder 651 | // twoLibertyTestAtEdgeOnly means that we will check the 2-liberty groups only 652 | // at the board edge, allowing check of the most common short ladders 653 | // even in the playouts 654 | func fixAtari(pos Position, c int, singlePointOK, twoLibertyTest, twoLibertyTestAtEdgeOnly bool) (bool, []int) { 655 | // check if a capturable ladder is being pulled out at c and return 656 | // a move that continues it in that case; expects its two liberties as 657 | // l1, l2 (in fact, this is a general 2-lib capture exhaustive solver) 658 | readLadderAttack := func(pos Position, c, l1, l2 int) int { 659 | for _, l := range []int{l1, l2} { 660 | pos_l, pos_err := pos.move(l) 661 | if pos_err != "ok" { 662 | continue 663 | } 664 | // fixAtari() will recursively call readLadderAttack() back; 665 | // however, ignore 2lib groups as we don't have time to chase them 666 | isAtari, atariEscape := fixAtari(pos_l, c, false, false, false) 667 | if isAtari && len(atariEscape) == 0 { 668 | return l 669 | } 670 | } 671 | return NONE 672 | } 673 | 674 | fillBoard := floodfill(pos.board, c) 675 | groupSize := bytes.Count(fillBoard, []byte{'#'}) 676 | if singlePointOK && groupSize == 1 { 677 | return false, []int{} 678 | } 679 | // Find a liberty 680 | l := contact(fillBoard, '.') 681 | // Ok, any other liberty? 682 | fillBoard = boardPut(fillBoard, l, 'L') 683 | l2 := contact(fillBoard, '.') 684 | if l2 != NONE { 685 | // At least two liberty group... 686 | if twoLibertyTest && groupSize > 1 && 687 | (!twoLibertyTestAtEdgeOnly || lineHeight(l) == 0 && lineHeight(l2) == 0) && 688 | contact(boardPut(fillBoard, l2, 'L'), '.') == NONE { 689 | // Exactly two liberty group with more than one stone. Check 690 | // that it cannot be caught in a working ladder; if it can, 691 | // that's as good as in atari, a capture threat. 692 | // (Almost - N/A for countercaptures.) 693 | ladderAttack := readLadderAttack(pos, c, l, l2) 694 | if ladderAttack >= 0 { 695 | return false, []int{ladderAttack} 696 | } 697 | } 698 | return false, []int{} 699 | } 700 | 701 | // In atari! If it's the opponent's group, that's enough... 702 | if pos.board[c] == 'x' { 703 | return true, []int{l} 704 | } 705 | 706 | solutions := []int{} 707 | 708 | // Before thinking about defense, what about counter-capturing 709 | // a neighboring group? 710 | counterCaptureBoard := fillBoard 711 | for { 712 | otherGroup := contact(counterCaptureBoard, 'x') 713 | if otherGroup == NONE { 714 | break 715 | } 716 | a, ccls := fixAtari(pos, otherGroup, false, false, false) 717 | if a && len(ccls) > 0 { 718 | solutions = append(solutions, ccls...) 719 | } 720 | // XXX: floodfill is better for big groups 721 | counterCaptureBoard = boardPut(counterCaptureBoard, otherGroup, '%') 722 | } 723 | 724 | // We are escaping. Will playing our last liberty gain 725 | // at least two liberties? Re-floodfill to account for connecting 726 | escpos, escerr := pos.move(l) 727 | if escerr != "ok" { 728 | return true, solutions // oops, suicidal move 729 | } 730 | fillBoard = floodfill(escpos.board, l) 731 | l_new := contact(fillBoard, '.') 732 | fillBoard = boardPut(fillBoard, l_new, 'L') 733 | l_new_2 := contact(fillBoard, '.') 734 | if l_new_2 != NONE { 735 | if len(solutions) > 0 || 736 | !(contact(boardPut(fillBoard, l_new_2, 'L'), '.') == NONE && 737 | readLadderAttack(escpos, l, l_new, l_new_2) != NONE) { 738 | solutions = append(solutions, l) 739 | } 740 | } 741 | return true, solutions 742 | } 743 | 744 | // return a board map listing common fate graph distances from 745 | // a given point - this corresponds to the concept of locality while 746 | // contracting groups to single points 747 | func cfgDistance(board []byte, c int) []int { 748 | cfg_map := []int{} 749 | for i := 0; i < W*W; i++ { 750 | cfg_map = append(cfg_map, NONE) 751 | } 752 | cfg_map[c] = 0 753 | 754 | // flood-fill like mechanics 755 | fringe := []int{c} 756 | for len(fringe) > 0 { 757 | c, fringe = fringe[len(fringe)-1], fringe[:len(fringe)-1] 758 | for _, d := range neighbors(c) { 759 | if isSpace(board[d]) || 760 | (0 <= cfg_map[d] && cfg_map[d] <= cfg_map[c]) { 761 | continue 762 | } 763 | cfg_before := cfg_map[d] 764 | if board[d] != '.' && board[d] == board[c] { 765 | cfg_map[d] = cfg_map[c] 766 | } else { 767 | cfg_map[d] = cfg_map[c] + 1 768 | } 769 | if cfg_before < 0 || cfg_before > cfg_map[d] { 770 | fringe = append(fringe, d) 771 | } 772 | } 773 | } 774 | return cfg_map 775 | } 776 | 777 | // Return the line number above nearest board edge 778 | func lineHeight(c int) int { 779 | row, col := (c-(W+1))/W, (c-(W+1))%W 780 | minVal := row 781 | for _, testVal := range []int{col, N - 1 - row, N - 1 - col} { 782 | if testVal < minVal { 783 | minVal = testVal 784 | } 785 | } 786 | return minVal 787 | } 788 | 789 | // Check whether there are any stones in Manhattan distance up to dist 790 | func emptyArea(board []byte, c, dist int) bool { 791 | for d := range neighbors(c) { 792 | if bytes.Contains([]byte{'X', 'x'}, []byte{board[d]}) { 793 | return false 794 | } 795 | if board[d] == '.' && dist > 1 && !emptyArea(board, d, dist-1) { 796 | return false 797 | } 798 | } 799 | return true 800 | } 801 | 802 | // return a string containing the 9 points forming 3x3 square around 803 | // certain move candidate 804 | func neighborhood3x3(board []byte, c int) []byte { 805 | localBoard := append([]byte{}, board[c-W-1:c-W+2]...) 806 | localBoard = append(localBoard, board[c-1:c+2]...) 807 | localBoard = append(localBoard, board[c+W-1:c+W+2]...) 808 | return bytes.Replace(localBoard, []byte{'\n'}, []byte{' '}, -1) 809 | } 810 | 811 | // large-scale pattern routines (those patterns living in patterns.{spat,prob} files) 812 | 813 | // are you curious how these patterns look in practice? get 814 | // https://github.com/pasky/pachi/blob/master/tools/pattern_spatial_show.pl 815 | // and try e.g. ./pattern_spatial_show.pl 71 816 | 817 | var spatPatternDict map[int]uint64 // hash(neighborhoodGridcular()) <- spatial id 818 | 819 | // load dictionary of positions, translating them to numeric ids 820 | func loadSpatPatternDict(f *os.File) { 821 | scanner := bufio.NewScanner(f) 822 | for scanner.Scan() { 823 | line := scanner.Text() 824 | // line: 71 6 ..X.X..OO.O..........#X...... 33408f5e 188e9d3e 2166befe aa8ac9e 127e583e 1282462e 5e3d7fe 51fc9ee 825 | if strings.HasPrefix(line, "#") { 826 | continue 827 | } 828 | lineFields := strings.SplitN(line, " ", 4) 829 | id, err := strconv.ParseInt(string(lineFields[0]), 10, 0) 830 | if err != nil { 831 | continue 832 | } 833 | neighborhood := strings.Replace(strings.Replace(lineFields[2], "#", " ", -1), "O", "x", -1) 834 | 835 | spatPatternDict[int(id)] = xxhash.Checksum64([]byte(neighborhood)) 836 | } 837 | } 838 | 839 | var largePatterns map[uint64]float64 // hash(neighborhoodGridcular()) -> probability 840 | 841 | // dictionary of numeric pattern ids, translating them to probabilities 842 | // that a move matching such move will be played when it is available 843 | func loadLargePatterns(f *os.File) { 844 | scanner := bufio.NewScanner(f) 845 | for scanner.Scan() { 846 | line := scanner.Text() 847 | // line: 0.004 14 3842 (capture:17 border:0 s:784) 848 | lineFields := strings.SplitN(line, " ", 4) 849 | 850 | prob, err := strconv.ParseFloat(lineFields[0], 64) 851 | if err != nil { 852 | continue 853 | } 854 | targetData := strings.SplitN(lineFields[3], "s:", 2) 855 | if len(targetData) < 2 { 856 | continue 857 | } 858 | dataStrings := strings.SplitN(targetData[1], ")", 2) 859 | if dataStrings == nil { 860 | continue 861 | } 862 | id, err := strconv.ParseInt(dataStrings[0], 10, 0) 863 | if err != nil { 864 | continue 865 | } 866 | patternHash, goodHash := spatPatternDict[int(id)] 867 | if !goodHash { 868 | continue 869 | } 870 | largePatterns[patternHash] = prob 871 | } 872 | } 873 | 874 | // Yield progressively wider-diameter gridcular board neighborhood 875 | // stone configuration strings, in all possible rotations 876 | func neighborhoodGridcular(board []byte, c int, done chan struct{}) chan []byte { 877 | ch := make(chan []byte) 878 | 879 | go func() { 880 | defer close(ch) 881 | // Each rotations element is (xyindex, xymultiplier) 882 | rotations := [][][]int{ 883 | {{0, 1}, {1, 1}}, 884 | {{0, 1}, {-1, 1}}, 885 | {{0, 1}, {1, -1}}, 886 | {{0, 1}, {-1, -1}}, 887 | {{1, 0}, {1, 1}}, 888 | {{1, 0}, {-1, 1}}, 889 | {{1, 0}, {1, -1}}, 890 | {{1, 0}, {-1, -1}}, 891 | } 892 | neighborhood := [][]byte{} 893 | for ri := 0; ri < len(rotations); ri++ { 894 | neighborhood = append(neighborhood, []byte{}) 895 | } 896 | wboard := bytes.Replace(board, []byte{'\n'}, []byte{' '}, -1) 897 | for _, dseq := range patternGridcularSequence { 898 | for ri := 0; ri < len(rotations); ri++ { 899 | r := rotations[ri] 900 | for _, o := range dseq { 901 | y, x := (c-(W+1))/W, (c-(W+1))%W 902 | y += o[r[0][0]] * r[1][0] 903 | x += o[r[0][1]] * r[1][1] 904 | if y >= 0 && y < N && x >= 0 && x < N { 905 | si := (y+1)*W + x + 1 906 | neighborhood[ri] = append(neighborhood[ri], wboard[si]) 907 | } else { 908 | neighborhood[ri] = append(neighborhood[ri], byte(' ')) 909 | } 910 | } 911 | select { 912 | case ch <- neighborhood[ri]: 913 | case <-done: 914 | return 915 | } 916 | } 917 | } 918 | // close(ch) deferred on entry to go func() 919 | }() 920 | return ch 921 | } 922 | 923 | // return probability of large-scale pattern at coordinate c. 924 | // Multiple progressively wider patterns may match a single coordinate, 925 | // we consider the largest one. 926 | func largePatternProbability(board []byte, c int) float64 { 927 | probability := float64(NONE) 928 | matchedLength := 0 929 | nonMatchedLength := 0 930 | done := make(chan struct{}) 931 | for n := range neighborhoodGridcular(board, c, done) { 932 | prob, good_prob := largePatterns[xxhash.Checksum64(n)] 933 | if good_prob { 934 | probability = prob 935 | matchedLength = len(n) 936 | continue 937 | } 938 | if matchedLength < nonMatchedLength && nonMatchedLength < len(n) { 939 | // stop when we did not match any pattern with a certain 940 | // diameter - it ain't going to get any better! 941 | break 942 | } 943 | nonMatchedLength = len(n) 944 | } 945 | close(done) 946 | return probability 947 | } 948 | 949 | //########################## 950 | // montecarlo playout policy 951 | 952 | // Yield candidate next moves in the order of preference; this is one 953 | // of the main places where heuristics dwell, try adding more! 954 | // 955 | // heuristicSet is the set of coordinates considered for applying heuristics; 956 | // this is the immediate neighborhood of last two moves in the playout, but 957 | // the whole board while prioring the tree. 958 | type Result struct { 959 | intResult int 960 | strResult string 961 | } 962 | 963 | func generatePlayoutMoves(pos Position, heuristicSet []int, probs map[string]float64, expensiveOK bool, done chan struct{}) chan Result { 964 | ch := make(chan Result) 965 | var r Result 966 | 967 | go func() { 968 | defer close(ch) 969 | rng := newRNG() 970 | // Check whether any local group is in atari and fill that liberty 971 | if rng.Float64() <= probs["capture"] { 972 | alreadySuggested := []int{} 973 | for _, c := range heuristicSet { 974 | if bytes.Contains([]byte{'X', 'x'}, []byte{pos.board[c]}) { 975 | _, ds := fixAtari(pos, c, false, true, !(expensiveOK)) 976 | shuffleInt(ds, rng) 977 | for _, d := range ds { 978 | if !(intInSlice(alreadySuggested, d)) { 979 | r.intResult = d 980 | r.strResult = "capture " + strconv.FormatInt(int64(c), 10) 981 | select { 982 | case ch <- r: 983 | case <-done: 984 | return 985 | } 986 | alreadySuggested = append(alreadySuggested, d) 987 | } 988 | } 989 | } 990 | } 991 | } 992 | 993 | // Try to apply a 3x3 pattern on the local neighborhood 994 | if rng.Float64() <= probs["pat3"] { 995 | alreadySuggested := []int{} 996 | for _, c := range heuristicSet { 997 | if pos.board[c] == '.' && !(intInSlice(alreadySuggested, c)) && patternInSet(pat3set, neighborhood3x3(pos.board, c)) { 998 | r.intResult = c 999 | r.strResult = "pat3" 1000 | select { 1001 | case ch <- r: 1002 | case <-done: 1003 | return 1004 | } 1005 | alreadySuggested = append(alreadySuggested, c) 1006 | } 1007 | } 1008 | } 1009 | 1010 | // Try *all* available moves, but starting from a random point 1011 | // (in other words, suggest a random move) 1012 | moves_done := make(chan struct{}) 1013 | defer close(moves_done) 1014 | x, y := rng.Intn(N-1)+1, rng.Intn(N-1)+1 1015 | for c := range pos.moves(y*W+x, moves_done) { 1016 | r.intResult = c 1017 | r.strResult = "random" 1018 | select { 1019 | case ch <- r: 1020 | case <-done: 1021 | return 1022 | } 1023 | } 1024 | // close(moves_done) defer'd 1025 | // close(ch) defer'd at start of routine 1026 | }() 1027 | return ch 1028 | } 1029 | 1030 | // Start a Monte Carlo playout from a given position, 1031 | // return score for to-play player at the starting position; 1032 | // amaf_map is board-sized scratchpad recording who played at a given 1033 | // position first 1034 | func mcplayout(pos Position, amaf_map []int, disp bool) (float64, []int, []float64) { 1035 | var err string 1036 | var pos2 Position 1037 | var prob_reject float64 1038 | if disp { 1039 | fmt.Fprintln(os.Stderr, "** SIMULATION **") 1040 | } 1041 | rng := newRNG() 1042 | 1043 | start_n := pos.n 1044 | passes := 0 1045 | for passes < 2 && pos.n < MAX_GAME_LEN { 1046 | if disp { 1047 | printPosition(pos, os.Stderr, nil) 1048 | } 1049 | pos2.n = NONE 1050 | // We simply try the moves our heuristics generate, in a particular 1051 | // order, but not with 100% probability; this is on the border between 1052 | // "rule-based playouts" and "probability distribution playouts". 1053 | done := make(chan struct{}) 1054 | for r := range generatePlayoutMoves(pos, pos.lastMovesNeighbors(rng), PROB_HEURISTIC, false, done) { 1055 | c := r.intResult 1056 | kind := r.strResult 1057 | if disp && kind != "random" { 1058 | fmt.Fprintln(os.Stderr, "move suggestion", stringCoordinates(c), kind) 1059 | } 1060 | pos2, err = pos.move(c) 1061 | if err != "ok" { 1062 | pos2.n = NONE 1063 | continue 1064 | } 1065 | // check if the suggested move did not turn out to be a self-atari 1066 | if kind == "random" { 1067 | prob_reject = PROB_RSAREJECT 1068 | } else { 1069 | prob_reject = PROB_SSAREJECT 1070 | } 1071 | if rng.Float64() <= prob_reject { 1072 | _, atariEscape := fixAtari(pos2, c, true, true, true) 1073 | if len(atariEscape) > 0 { 1074 | if disp { 1075 | fmt.Fprintln(os.Stderr, "rejecting self-atari move", stringCoordinates(c)) 1076 | } 1077 | pos2.n = NONE 1078 | continue 1079 | } 1080 | } 1081 | if amaf_map[c] == 0 { // Mark the coordinate with 1 for black 1082 | if pos.n%2 == 0 { 1083 | amaf_map[c] = 1 1084 | } else { 1085 | amaf_map[c] = -1 1086 | } 1087 | } 1088 | break 1089 | } 1090 | close(done) 1091 | if pos2.n == NONE { // no valid moves, pass 1092 | pos, _ = pos.passMove() 1093 | passes += 1 1094 | continue 1095 | } 1096 | passes = 0 1097 | pos = pos2 1098 | } 1099 | owner_map := make([]float64, W*W) 1100 | score := pos.score(owner_map) 1101 | if disp { 1102 | if pos.n%2 == 0 { 1103 | fmt.Fprintf(os.Stderr, "** SCORE B%+.1f **\n", score) 1104 | } else { 1105 | fmt.Fprintf(os.Stderr, "** SCORE B%+.1f **\n", -score) 1106 | } 1107 | } 1108 | if start_n%2 != pos.n%2 { 1109 | score = -score 1110 | } 1111 | return score, amaf_map, owner_map 1112 | } 1113 | 1114 | //####################### 1115 | // montecarlo tree search 1116 | 1117 | // Monte-Carlo tree node; 1118 | // v is #visits, w is #wins for to-play (expected reward is w/v) 1119 | // pv, pw are prior values (node value = w/v + pw/pv) 1120 | // av, aw are amaf values ("all moves as first", used for the RAVE tree policy) 1121 | // children is None for leaf nodes 1122 | type TreeNode struct { 1123 | pos Position 1124 | v int // # of visits 1125 | w int // # wins 1126 | pv int // prior value of v 1127 | pw int // prior value of w 1128 | av int 1129 | aw int 1130 | children []*TreeNode 1131 | } 1132 | 1133 | func NewTreeNode(pos Position) *TreeNode { 1134 | tn := new(TreeNode) 1135 | tn.pos = pos 1136 | tn.v = 0 1137 | tn.w = 0 1138 | tn.pv = PRIOR_EVEN 1139 | tn.pw = PRIOR_EVEN / 2 1140 | tn.av = 0 1141 | tn.aw = 0 1142 | tn.children = []*TreeNode{} 1143 | return tn 1144 | } 1145 | 1146 | // add and initialize children to a leaf node 1147 | func (tn *TreeNode) expand() { 1148 | cfg_map := []int{} 1149 | if tn.pos.last >= 0 { // there is actually a move 1150 | cfg_map = append(cfg_map, cfgDistance(tn.pos.board, tn.pos.last)...) 1151 | } 1152 | tn.children = []*TreeNode{} 1153 | childSet := map[int]*TreeNode{} 1154 | // Use playout generator to generate children and initialize them 1155 | // with some priors to bias search towards more sensible moves. 1156 | // Note that there can be many ways to incorporate the priors in 1157 | // next node selection (progressive bias, progressive widening, ...). 1158 | seedSet := []int{} 1159 | for i := N; i < (N+1)*W; i++ { 1160 | seedSet = append(seedSet, i) 1161 | } 1162 | done := make(chan struct{}) 1163 | for r := range generatePlayoutMoves(tn.pos, seedSet, map[string]float64{"capture": 1, "pat3": 1}, true, done) { 1164 | c := r.intResult 1165 | kind := r.strResult 1166 | pos2, err := tn.pos.move(c) 1167 | if err != "ok" { 1168 | continue 1169 | } 1170 | // generatePlayoutMoves() will generate duplicate suggestions 1171 | // if a move is yielded by multiple heuristics 1172 | node, ok := childSet[pos2.last] 1173 | if !ok { 1174 | node = NewTreeNode(pos2) 1175 | tn.children = append(tn.children, node) 1176 | childSet[pos2.last] = node 1177 | } 1178 | 1179 | if strings.HasPrefix(kind, "capture") { 1180 | // Check how big group we are capturing; coord of the group is 1181 | // second word in the ``kind`` string 1182 | coord, _ := strconv.ParseInt(strings.Split(kind, " ")[1], 10, 32) 1183 | if bytes.Count(floodfill(tn.pos.board, int(coord)), []byte{'#'}) > 1 { 1184 | node.pv += PRIOR_CAPTURE_MANY 1185 | node.pw += PRIOR_CAPTURE_MANY 1186 | } else { 1187 | node.pv += PRIOR_CAPTURE_ONE 1188 | node.pw += PRIOR_CAPTURE_ONE 1189 | } 1190 | } else if kind == "pat3" { 1191 | node.pv += PRIOR_PAT3 1192 | node.pw += PRIOR_PAT3 1193 | } 1194 | } 1195 | close(done) 1196 | 1197 | // Second pass setting priors, considering each move just once now 1198 | for _, node := range tn.children { 1199 | c := node.pos.last 1200 | 1201 | if len(cfg_map) > 0 && cfg_map[c]-1 < len(PRIOR_CFG) { 1202 | node.pv += PRIOR_CFG[cfg_map[c]-1] 1203 | node.pw += PRIOR_CFG[cfg_map[c]-1] 1204 | } 1205 | 1206 | height := lineHeight(c) // 0-indexed 1207 | if height <= 2 && emptyArea(tn.pos.board, c, 3) { 1208 | // No stones around; negative prior for 1st + 2nd line, positive 1209 | // for 3rd line; sanitizes opening and invasions 1210 | if height <= 1 { 1211 | node.pv += PRIOR_EMPTYAREA 1212 | node.pw += 0 1213 | } 1214 | if height == 2 { 1215 | node.pv += PRIOR_EMPTYAREA 1216 | node.pw += PRIOR_EMPTYAREA 1217 | } 1218 | } 1219 | 1220 | inAtari, _ := fixAtari(node.pos, c, true, true, false) 1221 | if inAtari { 1222 | node.pv += PRIOR_SELFATARI 1223 | node.pw += 0 // negative prior 1224 | } 1225 | 1226 | patternProbability := largePatternProbability(tn.pos.board, c) 1227 | if patternProbability > 0.001 { 1228 | patternPrior := math.Sqrt(patternProbability) // tone up 1229 | node.pv += int(patternPrior * PRIOR_LARGEPATTERN) 1230 | node.pw += int(patternPrior * PRIOR_LARGEPATTERN) 1231 | } 1232 | } 1233 | 1234 | if len(tn.children) == 0 { 1235 | // No possible moves, add a pass move 1236 | pass_pos, _ := tn.pos.passMove() 1237 | tn.children = append(tn.children, NewTreeNode(pass_pos)) 1238 | } 1239 | } 1240 | 1241 | func (tn *TreeNode) raveUrgency() float64 { 1242 | v := tn.v + tn.pv 1243 | expectation := float64(tn.w+tn.pw) / float64(v) 1244 | if tn.av == 0 { 1245 | return expectation 1246 | } 1247 | raveExpectation := float64(tn.aw) / float64(tn.av) 1248 | beta := float64(tn.av) / (float64(tn.av+v) + float64(v)*float64(tn.av)/RAVE_EQUIV) 1249 | return beta*raveExpectation + (1-beta)*expectation 1250 | } 1251 | 1252 | func (tn *TreeNode) winrate() float64 { 1253 | if tn.v > 0 { 1254 | return float64(tn.w) / float64(tn.v) 1255 | } else { 1256 | return math.NaN() 1257 | } 1258 | } 1259 | 1260 | // best move is the most simulated one 1261 | func (tn *TreeNode) bestMove() (*TreeNode, bool) { 1262 | var maxNode *TreeNode 1263 | if len(tn.children) == 0 { 1264 | return nil, false 1265 | } else { 1266 | max_v := -1 1267 | for _, node := range tn.children { 1268 | if node.v > max_v { 1269 | max_v = node.v 1270 | maxNode = node 1271 | } 1272 | } 1273 | } 1274 | return maxNode, true 1275 | } 1276 | 1277 | // Descend through the tree to a leaf 1278 | func treeDescend(tree *TreeNode, amaf_map []int, disp bool) []*TreeNode { 1279 | tree.v += 1 1280 | nodes := []*TreeNode{tree} 1281 | passes := 0 1282 | for len(nodes[len(nodes)-1].children) > 0 && passes < 2 { 1283 | if disp { 1284 | printPosition(nodes[len(nodes)-1].pos, os.Stderr, nil) 1285 | } 1286 | 1287 | // Pick the most urgent child 1288 | children := make([]*TreeNode, len(nodes[len(nodes)-1].children)) 1289 | copy(children, nodes[len(nodes)-1].children) 1290 | if disp { 1291 | for _, child := range children { 1292 | dumpSubtree(child, N_SIMS/50, 0, os.Stderr, false) 1293 | } 1294 | } 1295 | shuffleTree(children, rng) // randomize the max in case of equal urgency 1296 | 1297 | // find most urgent child by node.raveUrgency() 1298 | node := children[0] 1299 | maxRave := node.raveUrgency() 1300 | for i, c := range children { 1301 | if i == 0 { // skip item 0 as we already have its data 1302 | continue 1303 | } 1304 | testRave := c.raveUrgency() 1305 | if testRave > maxRave { 1306 | node = c 1307 | maxRave = testRave 1308 | } 1309 | } 1310 | nodes = append(nodes, node) 1311 | 1312 | if disp { 1313 | fmt.Fprintf(os.Stderr, "chosen %s\n", stringCoordinates(node.pos.last)) 1314 | } 1315 | if node.pos.last == PASS { 1316 | passes += 1 1317 | } else { 1318 | passes = 0 1319 | if amaf_map[node.pos.last] == 0 { // Mark the coordinate with 1 for black 1320 | if nodes[len(nodes)-2].pos.n%2 == 0 { 1321 | amaf_map[node.pos.last] = 1 1322 | } else { 1323 | amaf_map[node.pos.last] = -1 1324 | } 1325 | } 1326 | } 1327 | // updating visits on the way *down* represents "virtual loss", relevant for parallelization 1328 | node.v += 1 1329 | if len(node.children) == 0 && node.v >= EXPAND_VISITS { 1330 | node.expand() 1331 | } 1332 | } 1333 | return nodes 1334 | } 1335 | 1336 | // Store simulation result in the tree (@nodes is the tree path) 1337 | func treeUpdate(nodes []*TreeNode, amaf_map []int, score float64, disp bool) { 1338 | localNodes := nodes 1339 | for i, j := 0, len(localNodes)-1; i < j; i, j = i+1, j-1 { // reverse the order 1340 | localNodes[i], localNodes[j] = localNodes[j], localNodes[i] 1341 | } 1342 | for _, node := range localNodes { 1343 | if disp { 1344 | fmt.Fprintln(os.Stderr, "updating", stringCoordinates(node.pos.last), score < 0) 1345 | } 1346 | win := 0 1347 | if score < 0 { // score is for to-play, node statistics for just-played 1348 | win = 1 1349 | } 1350 | node.w += win 1351 | // Update the node children AMAF stats with moves we made 1352 | // with their color 1353 | amaf_map_value := 1 1354 | if node.pos.n%2 != 0 { 1355 | amaf_map_value = -1 1356 | } 1357 | if len(node.children) > 0 { 1358 | for _, child := range node.children { 1359 | if child.pos.last == PASS { 1360 | continue 1361 | } 1362 | if amaf_map[child.pos.last] == amaf_map_value { 1363 | if disp { 1364 | fmt.Fprintln(os.Stderr, " AMAF updating", stringCoordinates(child.pos.last), score > 0) 1365 | } 1366 | win = 0 1367 | if score > 0 { // reversed perspective 1368 | win = 1 1369 | } 1370 | child.aw += win 1371 | child.av += 1 1372 | } 1373 | } 1374 | } 1375 | score = -score 1376 | } 1377 | } 1378 | 1379 | // Perform MCTS search from a given position for a given #iterations 1380 | func treeSearch(tree *TreeNode, n int, owner_map []float64, disp bool) *TreeNode { 1381 | // Initialize root node 1382 | if len(tree.children) == 0 { 1383 | tree.expand() 1384 | } 1385 | 1386 | // We could simply run treeDescend(), mcplayout(), treeUpdate() 1387 | // sequentially in a loop. This is essentially what the code below 1388 | // does, if it seems confusing! 1389 | 1390 | // However, we also have an easy (though not optimal) way to parallelize 1391 | // by distributing the mcplayout() calls to other processes using the 1392 | // multiprocessing Python module. mcplayout() consumes maybe more than 1393 | // 90% CPU, especially on larger boards. (Except that with large patterns, 1394 | // expand() in the tree descent phase may be quite expensive - we can tune 1395 | // that tradeoff by adjusting the EXPAND_VISITS constant.) 1396 | 1397 | numWorkers := runtime.NumCPU() 1398 | if disp { // set to 1 when debugging 1399 | numWorkers = 1 1400 | } 1401 | 1402 | type Job struct { 1403 | nodes []*TreeNode 1404 | amaf_map []int 1405 | owner_map []float64 1406 | score float64 1407 | } 1408 | type JobResult struct { 1409 | n int 1410 | job Job 1411 | } 1412 | jr := make(chan JobResult) 1413 | outgoing := []Job{} // positions waiting for a playout 1414 | ongoing := map[int]Job{} // currently ongoing playout jobs 1415 | incoming := []Job{} //positions that finished evaluation 1416 | i := 0 1417 | for i < n { 1418 | if len(outgoing) == 0 && !(disp && len(ongoing) > 0) { 1419 | // Descend the tree so that we have something ready when a worker 1420 | // stops being busy 1421 | amaf_map := make([]int, W*W) 1422 | nodes := treeDescend(tree, amaf_map, disp) 1423 | var job Job 1424 | job.nodes = nodes 1425 | job.amaf_map = amaf_map 1426 | outgoing = append(outgoing, job) 1427 | } 1428 | 1429 | if len(ongoing) >= numWorkers { 1430 | // Too many playouts running? Wait a bit... 1431 | time.Sleep(10 * time.Millisecond / time.Duration(numWorkers)) 1432 | } else { 1433 | i += 1 1434 | if i > 0 && i%REPORT_PERIOD == 0 { 1435 | printTreeSummary(tree, i, os.Stderr) 1436 | } 1437 | 1438 | // Issue an mcplayout job to the worker pool 1439 | dispatch := outgoing[len(outgoing)-1] 1440 | outgoing = outgoing[:len(outgoing)-1] 1441 | jobnum := i 1442 | go func() { 1443 | var jobresult JobResult 1444 | jobresult.n = jobnum 1445 | jobresult.job = dispatch 1446 | jobresult.job.score, jobresult.job.amaf_map, jobresult.job.owner_map = mcplayout(jobresult.job.nodes[len(jobresult.job.nodes)-1].pos, jobresult.job.amaf_map, disp) 1447 | jr <- jobresult 1448 | }() 1449 | ongoing[jobnum] = dispatch 1450 | } 1451 | 1452 | // Anything to store in the tree? (We do this step out-of-order 1453 | // picking up data from the previous round so that we don't stall 1454 | // ready workers while we update the tree.) 1455 | for len(incoming) > 0 { 1456 | result := incoming[len(incoming)-1] 1457 | incoming = incoming[:len(incoming)-1] 1458 | treeUpdate(result.nodes, result.amaf_map, result.score, disp) 1459 | for c := 0; c < W*W; c++ { 1460 | owner_map[c] += result.owner_map[c] 1461 | } 1462 | } 1463 | 1464 | // Any playouts are finished yet? 1465 | select { 1466 | case jobresult := <-jr: // Yes! Queue them up for storing in the tree. 1467 | incoming = append(incoming, jobresult.job) 1468 | delete(ongoing, jobresult.n) 1469 | default: 1470 | } 1471 | 1472 | // Early stop test 1473 | best_move, ok := tree.bestMove() 1474 | if ok { 1475 | best_wr := best_move.winrate() 1476 | if (i > n/20 && best_wr > FASTPLAY5_THRES) || (i > n/5 && best_wr > FASTPLAY20_THRES) { 1477 | break 1478 | } 1479 | } 1480 | } 1481 | for len(ongoing) > 0 { // drain any pending background jobs 1482 | jobresult := <-jr 1483 | delete(ongoing, jobresult.n) 1484 | } 1485 | close(jr) 1486 | 1487 | for c := 0; c < W*W; c++ { 1488 | owner_map[c] = owner_map[c] / float64(i) 1489 | } 1490 | dumpSubtree(tree, N_SIMS/50, 0, os.Stderr, true) 1491 | printTreeSummary(tree, i, os.Stderr) 1492 | best_move, _ := tree.bestMove() 1493 | return best_move 1494 | } 1495 | 1496 | //################## 1497 | // user interface(s) 1498 | 1499 | // utility routines 1500 | 1501 | // print visualization of the given board position, optionally also 1502 | // including an owner map statistic (probability of that area of board 1503 | // eventually becoming black/white) 1504 | func printPosition(pos Position, f *os.File, owner_map []float64) { 1505 | var Xcap, Ocap int 1506 | var board []byte 1507 | if pos.n%2 == 0 { // to-play is black 1508 | board = bytes.Replace(pos.board, []byte{'x'}, []byte{'O'}, -1) 1509 | Xcap, Ocap = pos.cap[0], pos.cap[1] 1510 | } else { // to-play is white 1511 | board = bytes.Replace(bytes.Replace(pos.board, []byte{'X'}, []byte{'O'}, -1), []byte{'x'}, []byte{'X'}, -1) 1512 | Ocap, Xcap = pos.cap[0], pos.cap[1] 1513 | } 1514 | fmt.Fprintf(f, "Move: %-3d Black: %d caps White: %d caps Komi: %.1f\n", pos.n, Xcap, Ocap, pos.komi) 1515 | prettyBoard := strings.Join(strings.Split(string(board[:]), ""), " ") 1516 | if pos.last >= 0 { 1517 | prettyBoard = prettyBoard[:pos.last*2-1] + "(" + string(board[pos.last:pos.last+1]) + ")" + prettyBoard[pos.last*2+2:] 1518 | } 1519 | pb := []string{} 1520 | for i, row := range strings.Split(prettyBoard, "\n")[1 : N+1] { 1521 | row = fmt.Sprintf(" %-02d%s", N-i, row[2:]) 1522 | pb = append(pb, row) 1523 | } 1524 | prettyBoard = strings.Join(pb, "\n") 1525 | if len(owner_map) > 0 { 1526 | pretty_ownermap := "" 1527 | for c := 0; c < W*W-1; c++ { 1528 | if isSpace(board[c]) { 1529 | pretty_ownermap += string(board[c : c+1]) 1530 | } else if owner_map[c] > 0.6 { 1531 | pretty_ownermap += "X" 1532 | } else if owner_map[c] > 0.3 { 1533 | pretty_ownermap += "x" 1534 | } else if owner_map[c] < -0.6 { 1535 | pretty_ownermap += "O" 1536 | } else if owner_map[c] < -0.3 { 1537 | pretty_ownermap += "o" 1538 | } else { 1539 | pretty_ownermap += "." 1540 | } 1541 | } 1542 | pretty_ownermap = strings.Join(strings.Split(pretty_ownermap, ""), " ") 1543 | pb2 := []string{} 1544 | for i, orow := range strings.Split(pretty_ownermap, "\n")[1 : N+1] { 1545 | row := fmt.Sprintf("%s %s", pb[i], orow[1:]) 1546 | pb2 = append(pb2, row) 1547 | } 1548 | prettyBoard = strings.Join(pb2, "\n") 1549 | } 1550 | fmt.Fprintln(f, prettyBoard) 1551 | fmt.Fprintln(f, " "+strings.Join(strings.Split(columnString[:N], ""), " ")) 1552 | fmt.Fprintln(f, "") 1553 | } 1554 | 1555 | // Sort a slice of TreeNode by the v field starting with Max v 1556 | // Return sorted slice 1557 | // This replaces a sort using a lambda function in the original Python 1558 | func bestNodes(nodes []*TreeNode) []*TreeNode { 1559 | var i_max, v_max int 1560 | 1561 | if len(nodes) == 1 { 1562 | return nodes 1563 | } 1564 | i_max = -1 1565 | v_max = -1 1566 | for i, node := range nodes { 1567 | if node.v > v_max { 1568 | i_max = i 1569 | v_max = node.v 1570 | } 1571 | } 1572 | best_nodes := []*TreeNode{nodes[i_max]} 1573 | remaining_nodes := []*TreeNode{} 1574 | for i := 0; i < i_max; i++ { 1575 | remaining_nodes = append(remaining_nodes, nodes[i]) 1576 | } 1577 | for i := i_max + 1; i < len(nodes); i++ { 1578 | remaining_nodes = append(remaining_nodes, nodes[i]) 1579 | } 1580 | return append(best_nodes, bestNodes(remaining_nodes)...) 1581 | } 1582 | 1583 | // print this node and all its children with v >= thres. 1584 | func dumpSubtree(node *TreeNode, thres, indent int, f *os.File, recurse bool) { 1585 | var floatValue float64 1586 | if node.av > 0 { 1587 | floatValue = float64(node.aw) / float64(node.av) 1588 | } else { 1589 | floatValue = math.NaN() 1590 | } 1591 | fmt.Fprintf(f, "%s+- %s %.3f (%d/%d, prior %d/%d, rave %d/%d=%.3f, urgency %.3f)\n", 1592 | strings.Repeat(" ", indent), stringCoordinates(node.pos.last), node.winrate(), 1593 | node.w, node.v, node.pw, node.pv, node.aw, node.av, floatValue, 1594 | node.raveUrgency()) 1595 | if !recurse { 1596 | return 1597 | } 1598 | children := node.children 1599 | for _, child := range bestNodes(children) { 1600 | if child.v >= thres { 1601 | dumpSubtree(child, thres, indent+3, f, true) 1602 | } 1603 | } 1604 | } 1605 | 1606 | func printTreeSummary(tree *TreeNode, sims int, f *os.File) { 1607 | var exists bool 1608 | var best_nodes []*TreeNode 1609 | if len(tree.children) < 5 { 1610 | best_nodes = bestNodes(tree.children) 1611 | } else { 1612 | best_nodes = bestNodes(tree.children)[:5] 1613 | } 1614 | bestSequence := []int{} 1615 | node := tree 1616 | for { 1617 | bestSequence = append(bestSequence, node.pos.last) 1618 | node, exists = node.bestMove() 1619 | if !exists { // no children of current node 1620 | break 1621 | } 1622 | } 1623 | sequenceString := "" 1624 | if len(bestSequence) < 6 { 1625 | for i, c := range bestSequence { 1626 | if i == 0 { 1627 | continue 1628 | } 1629 | sequenceString += stringCoordinates(c) + " " 1630 | } 1631 | } else { 1632 | for _, c := range bestSequence[1:6] { 1633 | sequenceString += stringCoordinates(c) + " " 1634 | } 1635 | } 1636 | best_nodes_string := "" 1637 | for _, n := range best_nodes { 1638 | best_nodes_string += fmt.Sprintf("%s(%.3f) ", stringCoordinates(n.pos.last), n.winrate()) 1639 | } 1640 | if len(best_nodes) > 0 { 1641 | fmt.Fprintf(f, "[%4d] winrate %.3f | seq %s | can %s\n", sims, best_nodes[0].winrate(), 1642 | sequenceString, best_nodes_string) 1643 | } 1644 | } 1645 | 1646 | func parseCoordinates(s string) int { 1647 | if s == "pass" { 1648 | return PASS 1649 | } 1650 | row, _ := strconv.ParseInt(s[1:], 10, 32) 1651 | col := 1 + strings.Index(columnString, strings.ToUpper(s[0:1])) 1652 | c := W + (N-int(row))*W + col 1653 | return c 1654 | } 1655 | 1656 | func stringCoordinates(c int) string { 1657 | if c == PASS { 1658 | return "pass" 1659 | } 1660 | if c == NONE { 1661 | return "NONE" 1662 | } 1663 | row, col := (c-(W+1))/W, (c-(W+1))%W 1664 | return fmt.Sprintf("%c%d", columnString[col], N-row) 1665 | } 1666 | 1667 | // various main programs 1668 | 1669 | // run n Monte-Carlo playouts from empty position, return avg. score 1670 | func mcbenchmark(n int) float64 { 1671 | var scoreSum float64 1672 | for i := 0; i < n; i++ { 1673 | score, _, _ := mcplayout(emptyPosition(), make([]int, W*W), false) 1674 | scoreSum += score 1675 | } 1676 | return scoreSum / float64(n) 1677 | } 1678 | 1679 | // A simple minimalistic text mode UI. 1680 | func gameIO(computerBlack bool) { 1681 | reader := bufio.NewReader(os.Stdin) 1682 | tree := NewTreeNode(emptyPosition()) 1683 | tree.expand() 1684 | owner_map := make([]float64, W*W) 1685 | for { 1686 | if !(tree.pos.n == 0 && computerBlack) { 1687 | printPosition(tree.pos, os.Stdout, owner_map) 1688 | 1689 | fmt.Print("Your move: ") 1690 | sc, _ := reader.ReadString('\n') 1691 | sc = strings.TrimRight(sc, " \n") 1692 | c := parseCoordinates(sc) 1693 | if c >= 0 { 1694 | // Not a pass 1695 | if tree.pos.board[c] != '.' { 1696 | fmt.Println("Bad move (not empty point)") 1697 | continue 1698 | } 1699 | 1700 | // Find the next node in the game tree and proceed there 1701 | nodes := []*TreeNode{} 1702 | for _, node := range tree.children { 1703 | if node.pos.last == c { 1704 | nodes = append(nodes, node) 1705 | } 1706 | } 1707 | if len(nodes) == 0 { 1708 | fmt.Println("Bad move (rule violation)") 1709 | continue 1710 | } 1711 | tree = nodes[0] 1712 | } else { 1713 | // Pass move 1714 | if len(tree.children) > 0 && tree.children[0].pos.last == PASS { 1715 | tree = tree.children[0] 1716 | } else { 1717 | pos, _ := tree.pos.passMove() 1718 | tree = NewTreeNode(pos) 1719 | } 1720 | } 1721 | printPosition(tree.pos, os.Stdout, nil) 1722 | } 1723 | 1724 | owner_map = make([]float64, W*W) 1725 | tree = treeSearch(tree, N_SIMS, owner_map, false) 1726 | if tree.pos.last == PASS && tree.pos.last2 == PASS { 1727 | score := tree.pos.score(owner_map) 1728 | if tree.pos.n%2 == 1 { 1729 | score = -score 1730 | } 1731 | fmt.Printf("Game over, score: B%+.1f\n", score) 1732 | break 1733 | } 1734 | if float64(tree.w)/float64(tree.v) < RESIGN_THRES { 1735 | fmt.Println("I resign.") 1736 | break 1737 | } 1738 | } 1739 | fmt.Println("Thank you for the game!") 1740 | } 1741 | 1742 | // GTP interface for our program. We can play only on the board size 1743 | // which is configured (N), and we ignore color information and assume 1744 | // alternating play! 1745 | func gtpIO() { 1746 | gtpIn := bufio.NewScanner(os.Stdin) 1747 | knownCommands := []string{"boardsize", "clear_board", "komi", "play", 1748 | "genmove", "final_score", "quit", "name", 1749 | "version", "known_command", "list_commands", 1750 | "protocol_version", "tsdebug"} 1751 | 1752 | tree := NewTreeNode(emptyPosition()) 1753 | tree.expand() 1754 | 1755 | for gtpIn.Scan() { 1756 | line := gtpIn.Text() 1757 | line = strings.TrimRight(line, " \n") 1758 | if line == "" { 1759 | continue 1760 | } 1761 | line = strings.ToLower(line) 1762 | command := strings.Split(line, " ") 1763 | gtpCommandID := "" 1764 | matched, _ := regexp.MatchString("\\d+", command[0]) 1765 | if matched { 1766 | gtpCommandID = command[0] 1767 | command = command[1:] 1768 | } 1769 | owner_map := make([]float64, W*W) 1770 | ret := "" 1771 | if command[0] == "boardsize" { 1772 | size, _ := strconv.ParseInt(command[1], 10, 0) 1773 | if int(size) != N { 1774 | fmt.Fprintf(os.Stderr, "Warning: Trying to set incompatible boardsize %s (!= %d)\n", command[0], N) 1775 | ret = "None" 1776 | } 1777 | } else if command[0] == "clear_board" { 1778 | tree = NewTreeNode(emptyPosition()) 1779 | tree.expand() 1780 | } else if command[0] == "komi" { 1781 | komi, err := strconv.ParseFloat(command[1], 64) 1782 | if err == nil { 1783 | tree.pos.komi = komi 1784 | } else { 1785 | ret = "None" 1786 | } 1787 | } else if command[0] == "play" { 1788 | c := parseCoordinates(command[2]) 1789 | if c >= 0 { 1790 | // Find the next node in the game tree and proceed there 1791 | nodes := []*TreeNode{} 1792 | if len(tree.children) > 0 { 1793 | for _, node := range tree.children { 1794 | if node.pos.last == c { 1795 | nodes = append(nodes, node) 1796 | } 1797 | } 1798 | } 1799 | pos, err := tree.pos.move(c) 1800 | if err == "ok" { 1801 | tree = NewTreeNode(pos) 1802 | } else { 1803 | fmt.Fprintln(os.Stderr, "Error updating sent move:", err) 1804 | ret = "None" 1805 | } 1806 | } else { 1807 | // Pass move 1808 | if len(tree.children) > 0 && tree.children[0].pos.last == PASS { 1809 | tree = tree.children[0] 1810 | } else { 1811 | pos, _ := tree.pos.passMove() 1812 | tree = NewTreeNode(pos) 1813 | } 1814 | } 1815 | } else if command[0] == "genmove" { 1816 | tree = treeSearch(tree, N_SIMS, owner_map, false) 1817 | if tree.pos.last == PASS { 1818 | ret = "pass" 1819 | } else if tree.v > 0 && float64(tree.w)/float64(tree.v) < RESIGN_THRES { 1820 | ret = "resign" 1821 | } else { 1822 | ret = stringCoordinates(tree.pos.last) 1823 | } 1824 | } else if command[0] == "final_score" { 1825 | score := tree.pos.score([]float64{}) 1826 | if tree.pos.n%2 == 1 { 1827 | score = -score 1828 | } 1829 | if score == 0 { 1830 | ret = "0" 1831 | } else if score > 0 { 1832 | ret = fmt.Sprintf("B+%.1f", score) 1833 | } else if score < 0 { 1834 | ret = fmt.Sprintf("W+%.1f", -score) 1835 | } 1836 | } else if command[0] == "name" { 1837 | ret = "michi-go" 1838 | } else if command[0] == "version" { 1839 | ret = "3.0.0" 1840 | } else if command[0] == "tsdebug" { 1841 | printPosition(treeSearch(tree, N_SIMS, owner_map, true).pos, os.Stderr, nil) 1842 | } else if command[0] == "list_commands" { 1843 | ret = strings.Join(knownCommands, "\n") 1844 | } else if command[0] == "known_command" { 1845 | ret = "false" 1846 | for _, known := range knownCommands { 1847 | if command[1] == known { 1848 | ret = "true" 1849 | break 1850 | } 1851 | } 1852 | } else if command[0] == "protocol_version" { 1853 | ret = "2" 1854 | } else if command[0] == "quit" { 1855 | fmt.Printf("=%s \n\n", gtpCommandID) 1856 | break 1857 | } else { 1858 | fmt.Fprintln(os.Stderr, "Warning: Ignoring unknown command -", line) 1859 | ret = "None" 1860 | } 1861 | 1862 | printPosition(tree.pos, os.Stderr, owner_map) 1863 | if ret != "None" { 1864 | fmt.Printf("=%s %s\n\n", gtpCommandID, ret) 1865 | } else { 1866 | fmt.Printf("?%s ???\n\n", gtpCommandID) 1867 | } 1868 | } 1869 | } 1870 | 1871 | func main() { 1872 | performInitialization() 1873 | patternLoadError := false 1874 | f, err := os.Open(spatPatternDictFile) 1875 | if err == nil { 1876 | fmt.Fprintln(os.Stderr, "Loading pattern spatial dictionary...") 1877 | loadSpatPatternDict(f) 1878 | } else { 1879 | patternLoadError = true 1880 | fmt.Fprintln(os.Stderr, "Error opening ", spatPatternDictFile, err) 1881 | } 1882 | f, err = os.Open(largePatternsFile) 1883 | if err == nil { 1884 | fmt.Fprintln(os.Stderr, "Loading large patterns...") 1885 | loadLargePatterns(f) 1886 | } else { 1887 | patternLoadError = true 1888 | fmt.Fprintln(os.Stderr, "Error opening ", largePatternsFile, err) 1889 | } 1890 | if patternLoadError { 1891 | fmt.Fprintln(os.Stderr, "Warning: Cannot load pattern files; will be much weaker, consider lowering EXPAND_VISITS 5->2") 1892 | } 1893 | fmt.Fprintln(os.Stderr, "Done") 1894 | 1895 | rng = newRNG() 1896 | 1897 | if len(os.Args) < 2 { 1898 | // Default action 1899 | gameIO(false) 1900 | } else if os.Args[1] == "white" { 1901 | gameIO(true) 1902 | } else if os.Args[1] == "gtp" { 1903 | gtpIO() 1904 | } else if os.Args[1] == "mcdebug" { 1905 | score, _, _ := mcplayout(emptyPosition(), make([]int, W*W), true) 1906 | fmt.Println(score) 1907 | } else if os.Args[1] == "mcbenchmark" { 1908 | fmt.Println(mcbenchmark(20)) 1909 | } else if os.Args[1] == "tsbenchmark" { 1910 | startTime := time.Now() 1911 | printPosition(treeSearch(NewTreeNode(emptyPosition()), N_SIMS, make([]float64, W*W), false).pos, os.Stderr, nil) 1912 | endTime := time.Now() 1913 | fmt.Printf("Tree search with %d playouts took %s with %d threads; speed is %.3f playouts/thread/s\n", 1914 | N_SIMS, endTime.Sub(startTime).String(), runtime.GOMAXPROCS(0), 1915 | float64(N_SIMS)/(endTime.Sub(startTime).Seconds()*float64(runtime.GOMAXPROCS(0)))) 1916 | } else if os.Args[1] == "tsdebug" { 1917 | printPosition(treeSearch(NewTreeNode(emptyPosition()), N_SIMS, make([]float64, W*W), true).pos, os.Stderr, nil) 1918 | } else { 1919 | fmt.Fprintln(os.Stderr, "Unknown action") 1920 | } 1921 | } 1922 | --------------------------------------------------------------------------------