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  }