github.com/gorgonia/agogo@v0.1.1/game/wq/gtp.go (about) 1 package 围碁 2 3 import ( 4 "bufio" 5 "fmt" 6 "os" 7 "strconv" 8 "strings" 9 "unicode" 10 11 "github.com/gorgonia/agogo/game" 12 ) 13 14 // Note this is a default implementation that has not yet been customised for our purposes 15 // GTPBoard will need to be replaced with AgogoBoard and have the associated GTP commands integrated with it 16 // Need to add func to agogo.go that calls StartGTP when new game is initialised 17 // Quoting chewxy, need a `GTPCapableState` struct that embeds `game.State` or `*wq.Game` and upon which we can Apply state changes 18 // Possibly split file out into gtp_wrapper where StartGTP lives and gtp_engine with the interface between agogo & GTP clients, similar to minigo 19 20 const ( 21 BORDER = -1 22 EMPTY = 0 23 BLACK = 1 // Don't change 24 WHITE = 2 // these now 25 ) 26 27 type Point struct { 28 X int 29 Y int 30 } 31 32 type AgogoBoard struct { 33 State game.State 34 } 35 36 type GTPBoard struct { 37 State [][]int 38 Ko Point 39 Size int 40 Komi float64 41 NextPlayer int 42 CapsByBlack int 43 CapsByWhite int 44 } 45 46 var known_commands = []string{ 47 "boardsize", "clear_board", "genmove", "known_command", "komi", "list_commands", 48 "name", "play", "protocol_version", "quit", "showboard", "undo", "version", 49 } 50 51 var bw_strings = []string{"??", "Black", "White"} // Relies on BLACK == 1 and WHITE == 2 52 53 func NewGTPBoard(size int, komi float64) *GTPBoard { 54 var board GTPBoard 55 board.Size = size 56 board.Komi = komi 57 board.Clear() 58 return &board 59 } 60 61 func (b *GTPBoard) Clear() { 62 63 // GTPBoard arrays are 2D arrays of (size + 2) x (size + 2) 64 // with explicit borders. 65 66 b.State = make([][]int, b.Size+2) 67 for i := 0; i < b.Size+2; i++ { 68 b.State[i] = make([]int, b.Size+2) 69 } 70 for i := 0; i < b.Size+2; i++ { 71 b.State[i][0] = BORDER 72 b.State[i][b.Size+1] = BORDER 73 b.State[0][i] = BORDER 74 b.State[b.Size+1][i] = BORDER 75 } 76 b.Ko.X = -1 77 b.Ko.Y = -1 78 b.NextPlayer = BLACK 79 b.CapsByBlack = 0 80 b.CapsByWhite = 0 81 } 82 83 func (b *GTPBoard) Copy() *GTPBoard { 84 newboard := NewGTPBoard(b.Size, b.Komi) // Does the borders for us, as well as size and komi 85 for y := 1; y <= b.Size; y++ { 86 for x := 1; x <= b.Size; x++ { 87 newboard.State[x][y] = b.State[x][y] 88 } 89 } 90 newboard.Ko.X = b.Ko.X 91 newboard.Ko.Y = b.Ko.Y 92 newboard.NextPlayer = b.NextPlayer 93 newboard.CapsByBlack = b.CapsByBlack 94 newboard.CapsByWhite = b.CapsByWhite 95 return newboard 96 } 97 98 func (b *GTPBoard) String() string { 99 s := "Current board:\n" 100 for y := 1; y <= b.Size; y++ { 101 for x := 1; x <= b.Size; x++ { 102 c := '.' 103 if b.Ko.X == x && b.Ko.Y == y { 104 c = '*' 105 } 106 if b.State[x][y] == BLACK { 107 c = 'X' 108 } else if b.State[x][y] == WHITE { 109 c = 'O' 110 } 111 s += fmt.Sprintf("%c ", c) 112 } 113 s += "\n" 114 } 115 s += fmt.Sprintf("Captures by Black: %d\n", b.CapsByBlack) 116 s += fmt.Sprintf("Captures by White: %d\n", b.CapsByWhite) 117 s += fmt.Sprintf("Komi: %.1f\n", b.Komi) 118 s += fmt.Sprintf("Next: %s\n", bw_strings[b.NextPlayer]) 119 s += "\n" 120 return s 121 } 122 123 func (b *GTPBoard) Dump() { // For debug only 124 fmt.Printf(b.String()) 125 } 126 127 func (b *GTPBoard) PlayMove(colour, x, y int) error { 128 129 if colour != BLACK && colour != WHITE { 130 return fmt.Errorf("colour neither black nor white") 131 } 132 133 var opponent_colour int 134 135 if colour == BLACK { 136 opponent_colour = WHITE 137 } else { 138 opponent_colour = BLACK 139 } 140 141 if x < 1 || x > b.Size || y < 1 || y > b.Size { 142 return fmt.Errorf("coordinate off board") 143 } 144 145 if b.State[x][y] != EMPTY { 146 return fmt.Errorf("coordinate not empty") 147 } 148 149 // Disallow playing on the ko square... 150 151 if colour == b.NextPlayer && b.Ko.X == x && b.Ko.Y == y { 152 return fmt.Errorf("illegal ko recapture") 153 } 154 155 // We must make the move here, AFTER the ko check... 156 157 b.State[x][y] = colour 158 159 // Normal captures... 160 161 last_point_captured := Point{-1, -1} // If we captured exactly 1 stone, this will record it 162 163 stones_destroyed := 0 164 adj_points := AdjacentPoints(x, y) 165 166 for _, point := range adj_points { 167 if b.State[point.X][point.Y] == opponent_colour { 168 if b.GroupHasLiberties(point.X, point.Y) == false { 169 stones_destroyed += b.destroy_group(point.X, point.Y) 170 last_point_captured = Point{point.X, point.Y} 171 } 172 } 173 } 174 175 // Disallow moves with no liberties (obviously after captures have been done)... 176 177 if b.GroupHasLiberties(x, y) == false { 178 b.State[x][y] = EMPTY 179 return fmt.Errorf("move is suicidal") 180 } 181 182 // A square is a ko square if: 183 // - It was the site of the only stone captured this turn 184 // - The capturing stone has no friendly neighbours 185 // - The capturing stone has one liberty 186 187 b.Ko.X = -1 188 b.Ko.Y = -1 189 190 if stones_destroyed == 1 { 191 192 // Provisonally set the ko square to be the captured square... 193 194 b.Ko.X = last_point_captured.X 195 b.Ko.Y = last_point_captured.Y 196 197 // But unset it if the capturing stone has any friendly neighbours or > 1 liberty 198 199 liberties := 0 200 friend_flag := false 201 202 for _, point := range adj_points { 203 if b.State[point.X][point.Y] == EMPTY { 204 liberties += 1 205 } 206 if b.State[point.X][point.Y] == colour { 207 friend_flag = true 208 break 209 } 210 } 211 212 if friend_flag || liberties > 1 { 213 b.Ko.X = -1 214 b.Ko.Y = -1 215 } 216 } 217 218 // Update some board info... 219 220 if colour == BLACK { 221 b.NextPlayer = WHITE 222 b.CapsByBlack += stones_destroyed 223 } else { 224 b.NextPlayer = BLACK 225 b.CapsByWhite += stones_destroyed 226 } 227 228 return nil 229 } 230 231 func (b *GTPBoard) GroupHasLiberties(x, y int) bool { 232 233 if x < 1 || y < 1 || x > b.Size || y > b.Size { 234 panic("GroupHasLiberties() called with illegal x,y") 235 } 236 237 checked_stones := make(map[Point]bool) 238 return b.group_has_liberties(x, y, checked_stones) 239 } 240 241 func (b *GTPBoard) group_has_liberties(x, y int, checked_stones map[Point]bool) bool { 242 243 checked_stones[Point{x, y}] = true 244 245 adj_points := AdjacentPoints(x, y) 246 247 for _, adj := range adj_points { 248 if b.State[adj.X][adj.Y] == EMPTY { 249 return true 250 } 251 } 252 253 for _, adj := range adj_points { 254 if b.State[adj.X][adj.Y] == b.State[x][y] { 255 if checked_stones[Point{adj.X, adj.Y}] == false { 256 if b.group_has_liberties(adj.X, adj.Y, checked_stones) { 257 return true 258 } 259 } 260 } 261 } 262 263 return false 264 } 265 266 func (b *GTPBoard) destroy_group(x, y int) int { 267 268 if x < 1 || y < 1 || x > b.Size || y > b.Size { 269 panic("destroy_group() called with illegal x,y") 270 } 271 272 stones_destroyed := 1 273 colour := b.State[x][y] 274 b.State[x][y] = EMPTY 275 276 for _, adj := range AdjacentPoints(x, y) { 277 if b.State[adj.X][adj.Y] == colour { 278 stones_destroyed += b.destroy_group(adj.X, adj.Y) 279 } 280 } 281 282 return stones_destroyed 283 } 284 285 func (b *GTPBoard) Pass(colour int) error { 286 287 if colour != BLACK && colour != WHITE { 288 return fmt.Errorf("colour neither black nor white") 289 } 290 291 b.Ko.X = -1 292 b.Ko.Y = -1 293 if colour == BLACK { 294 b.NextPlayer = WHITE 295 } else { 296 b.NextPlayer = BLACK 297 } 298 299 return nil 300 } 301 302 func (b *GTPBoard) NewFromMove(colour, x, y int) (*GTPBoard, error) { 303 newboard := b.Copy() 304 err := newboard.PlayMove(colour, x, y) 305 if err != nil { 306 return nil, err 307 } 308 return newboard, nil 309 } 310 311 func (b *GTPBoard) NewFromPass(colour int) (*GTPBoard, error) { 312 newboard := b.Copy() 313 err := newboard.Pass(colour) 314 if err != nil { 315 return nil, err 316 } 317 return newboard, nil 318 } 319 320 func (b *GTPBoard) AllLegalMoves(colour int) []Point { 321 322 if colour != BLACK && colour != WHITE { 323 return nil 324 } 325 326 var all_possible []Point 327 328 for x := 1; x <= b.Size; x++ { 329 330 Y_LOOP: 331 for y := 1; y <= b.Size; y++ { 332 333 if b.State[x][y] != EMPTY { 334 continue 335 } 336 337 for _, point := range AdjacentPoints(x, y) { 338 if b.State[point.X][point.Y] == EMPTY { 339 all_possible = append(all_possible, Point{x, y}) // Move is clearly legal since some of its neighbours are empty 340 continue Y_LOOP 341 } 342 } 343 344 // The move we are playing will have no liberties of its own. 345 // So check it by trying it. This is crude... 346 347 _, err := b.NewFromMove(colour, x, y) 348 if err == nil { 349 all_possible = append(all_possible, Point{x, y}) 350 } 351 } 352 } 353 354 return all_possible 355 } 356 357 func (b *GTPBoard) StringFromXY(x, y int) string { 358 letter := 'A' + x - 1 359 if letter >= 'I' { 360 letter += 1 361 } 362 number := b.Size + 1 - y 363 return fmt.Sprintf("%c%d", letter, number) 364 } 365 366 func (b *GTPBoard) StringFromPoint(p Point) string { 367 return b.StringFromXY(p.X, p.Y) 368 } 369 370 func (b *GTPBoard) XYFromString(s string) (int, int, error) { 371 372 if len(s) < 2 { 373 return -1, -1, fmt.Errorf("coordinate string too short") 374 } 375 376 letter := strings.ToLower(s)[0] 377 378 if letter < 'a' || letter > 'z' { 379 return -1, -1, fmt.Errorf("letter part of coordinate not in range a-z") 380 } 381 382 if letter == 'i' { 383 return -1, -1, fmt.Errorf("letter i not permitted") 384 } 385 386 x := int((letter - 'a') + 1) 387 if letter > 'i' { 388 x -= 1 389 } 390 391 tmp, err := strconv.Atoi(s[1:]) 392 if err != nil { 393 return -1, -1, fmt.Errorf("couldn't parse number part of coordinate") 394 } 395 y := (b.Size + 1 - tmp) 396 397 if x > b.Size || y > b.Size || x < 1 || y < 1 { 398 return -1, -1, fmt.Errorf("coordinate off board") 399 } 400 401 return x, y, nil 402 } 403 404 func AdjacentPoints(x, y int) []Point { 405 return []Point{Point{x - 1, y}, Point{x + 1, y}, Point{x, y - 1}, Point{x, y + 1}} 406 } 407 408 func StartGTP(genmove func(colour int, board *GTPBoard) string, name string, version string) { 409 410 var history []*GTPBoard 411 412 board := NewGTPBoard(19, 0.0) 413 414 scanner := bufio.NewScanner(os.Stdin) 415 416 for { 417 scanner.Scan() 418 line := scanner.Text() 419 line = strings.TrimSpace(line) 420 line = strings.ToLower(line) // Note this lowercase conversion 421 tokens := strings.Fields(line) 422 423 if len(tokens) == 0 { 424 continue 425 } 426 427 var id int = -1 428 429 if unicode.IsDigit(rune(tokens[0][0])) { 430 var err error 431 id, err = strconv.Atoi(tokens[0]) 432 if err != nil { 433 fmt.Printf("? Couldn't parse ID\n\n") 434 continue 435 } 436 tokens = tokens[1:] 437 } 438 439 if len(tokens) == 0 { 440 continue // This is GNU Go's behaviour when receiving just an ID 441 } 442 443 // So, by now, tokens is a list of the actual command; meanwhile id (if any) is saved 444 // -------------------------------------------------------------------------------------------------- 445 446 if tokens[0] == "name" { 447 print_success(id, name) 448 continue 449 } 450 451 // -------------------------------------------------------------------------------------------------- 452 453 if tokens[0] == "version" { 454 print_success(id, version) 455 continue 456 } 457 458 // -------------------------------------------------------------------------------------------------- 459 460 if tokens[0] == "protocol_version" { 461 print_success(id, "2") 462 continue 463 } 464 465 // -------------------------------------------------------------------------------------------------- 466 467 if tokens[0] == "list_commands" { 468 response := "" 469 for _, command := range known_commands { 470 response += command + "\n" 471 } 472 print_success(id, response) 473 continue 474 } 475 476 // -------------------------------------------------------------------------------------------------- 477 478 if tokens[0] == "known_command" { 479 if len(tokens) < 2 { 480 print_failure(id, "no argument received for known_command") 481 continue 482 } 483 response := "false" 484 for _, command := range known_commands { 485 if command == tokens[1] { 486 response = "true" 487 break 488 } 489 } 490 print_success(id, response) 491 continue 492 } 493 494 // -------------------------------------------------------------------------------------------------- 495 496 if tokens[0] == "komi" { 497 if len(tokens) < 2 { 498 print_failure(id, "no argument received for komi") 499 continue 500 } 501 komi, err := strconv.ParseFloat(tokens[1], 64) 502 if err != nil { 503 print_failure(id, "couldn't parse komi float") 504 continue 505 } 506 507 board.Komi = komi 508 for i := 0; i < len(history); i++ { // Since komi is in the boards, change it through history 509 history[i].Komi = komi 510 } 511 512 print_success(id, "") 513 continue 514 } 515 516 // -------------------------------------------------------------------------------------------------- 517 518 if tokens[0] == "clear_board" { 519 board.Clear() 520 history = nil 521 print_success(id, "") 522 continue 523 } 524 525 // -------------------------------------------------------------------------------------------------- 526 527 if tokens[0] == "quit" { 528 print_success(id, "") 529 os.Exit(0) 530 } 531 532 // -------------------------------------------------------------------------------------------------- 533 534 if tokens[0] == "showboard" { 535 print_success(id, board.String()) 536 continue 537 } 538 539 // -------------------------------------------------------------------------------------------------- 540 541 if tokens[0] == "boardsize" { 542 if len(tokens) < 2 { 543 print_failure(id, "no argument received for boardsize") 544 continue 545 } 546 size, err := strconv.Atoi(tokens[1]) 547 if err != nil { 548 print_failure(id, "couldn't parse boardsize int") 549 continue 550 } 551 if size < 3 || size > 26 { 552 print_failure(id, "boardsize not in range 3 - 26") 553 continue 554 } 555 board = NewGTPBoard(size, board.Komi) 556 history = nil 557 print_success(id, "") 558 continue 559 } 560 561 // -------------------------------------------------------------------------------------------------- 562 563 if tokens[0] == "play" { 564 565 if len(tokens) < 3 { 566 print_failure(id, "insufficient arguments received for play") 567 continue 568 } 569 570 if tokens[1] != "black" && tokens[1] != "b" && tokens[1] != "white" && tokens[1] != "w" { 571 print_failure(id, "did not understand colour for play") 572 continue 573 } 574 575 var colour int 576 if tokens[1][0] == 'w' { 577 colour = WHITE 578 } else { 579 colour = BLACK 580 } 581 582 if tokens[2] == "pass" { 583 584 newboard, _ := board.NewFromPass(colour) 585 history = append(history, board) 586 board = newboard 587 588 } else { 589 590 x, y, err := board.XYFromString(tokens[2]) 591 if err != nil { 592 print_failure(id, err.Error()) 593 continue 594 } 595 596 newboard, err := board.NewFromMove(colour, x, y) 597 if err != nil { 598 print_failure(id, err.Error()) 599 continue 600 } 601 602 history = append(history, board) 603 board = newboard 604 } 605 606 print_success(id, "") 607 continue 608 } 609 610 // -------------------------------------------------------------------------------------------------- 611 612 if tokens[0] == "genmove" { 613 614 if len(tokens) < 2 { 615 print_failure(id, "no argument received for genmove") 616 continue 617 } 618 619 if tokens[1] != "black" && tokens[1] != "b" && tokens[1] != "white" && tokens[1] != "w" { 620 print_failure(id, "did not understand colour for genmove") 621 continue 622 } 623 624 var colour int 625 if tokens[1][0] == 'w' { 626 colour = WHITE 627 } else { 628 colour = BLACK 629 } 630 631 s := genmove(colour, board.Copy()) // Send the engine a copy, not the real thing 632 633 if s == "pass" { 634 635 newboard, _ := board.NewFromPass(colour) 636 history = append(history, board) 637 board = newboard 638 639 } else { 640 641 x, y, err := board.XYFromString(s) 642 if err != nil { 643 print_failure(id, fmt.Sprintf("illegal move from engine: %s (%v)", s, err)) 644 continue 645 } 646 647 newboard, err := board.NewFromMove(colour, x, y) 648 if err != nil { 649 print_failure(id, fmt.Sprintf("illegal move from engine: %s (%v)", s, err)) 650 continue 651 } 652 653 history = append(history, board) 654 board = newboard 655 } 656 657 print_success(id, s) 658 continue 659 } 660 661 // -------------------------------------------------------------------------------------------------- 662 663 if tokens[0] == "undo" { 664 665 if len(history) == 0 { 666 print_failure(id, "cannot undo") 667 continue 668 } else { 669 board = history[len(history)-1] 670 history = history[0 : len(history)-1] 671 print_success(id, "") 672 continue 673 } 674 } 675 676 // -------------------------------------------------------------------------------------------------- 677 678 print_failure(id, "unknown command") 679 } 680 } 681 682 //func InitGTP(gamestate, game.State) GTPEncoding gtpencoding { 683 // return 684 //} 685 686 func print_reply(id int, s string, shebang string) { 687 s = strings.TrimSpace(s) 688 fmt.Printf(shebang) 689 if id != -1 { 690 fmt.Printf("%d", id) 691 } 692 if s != "" { 693 fmt.Printf(" %s\n\n", s) 694 } else { 695 fmt.Printf("\n\n") 696 } 697 } 698 699 func print_success(id int, s string) { 700 print_reply(id, s, "=") 701 } 702 703 func print_failure(id int, s string) { 704 print_reply(id, s, "?") 705 }