package main import ( "bufio" "bytes" "encoding/base64" "fmt" "log" "os/exec" "strconv" "strings" "time" "code.rocket9labs.com/tslocum/bgammon" ) func reverseString(s string) string { runes := []rune(s) for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { runes[i], runes[j] = runes[j], runes[i] } return string(runes) } func gnubgPosition(g *bgammon.Game) string { var opponent int8 = 2 start := 0 end := 25 boardStart := 1 boardEnd := 24 delta := 1 playerBarSpace := bgammon.SpaceBarPlayer opponentBarSpace := bgammon.SpaceBarOpponent switch g.Turn { case 1: case 2: opponent = 1 start = 25 end = 0 boardStart = 24 boardEnd = 1 delta = -1 playerBarSpace = bgammon.SpaceBarOpponent opponentBarSpace = bgammon.SpaceBarPlayer default: log.Fatalf("failed to analyze game: zero turn") } var buf []byte for space := boardStart; space != end; space += delta { playerCheckers := bgammon.PlayerCheckers(g.Board[space], g.Turn) for i := int8(0); i < playerCheckers; i++ { buf = append(buf, '1') } buf = append(buf, '0') } playerCheckers := bgammon.PlayerCheckers(g.Board[playerBarSpace], g.Turn) for i := int8(0); i < playerCheckers; i++ { buf = append(buf, '1') } buf = append(buf, '0') for space := boardEnd; space != start; space -= delta { opponentCheckers := bgammon.PlayerCheckers(g.Board[space], opponent) for i := int8(0); i < opponentCheckers; i++ { buf = append(buf, '1') } buf = append(buf, '0') } opponentCheckers := bgammon.PlayerCheckers(g.Board[opponentBarSpace], opponent) for i := int8(0); i < opponentCheckers; i++ { buf = append(buf, '1') } buf = append(buf, '0') for i := len(buf); i < 80; i++ { buf = append(buf, '0') } var out []byte for i := 0; i < len(buf); i += 8 { s := reverseString(string(buf[i : i+8])) v, err := strconv.ParseUint(s, 2, 8) if err != nil { panic(err) } out = append(out, byte(v)) } position := base64.StdEncoding.EncodeToString(out) if len(position) == 0 { return "" } for position[len(position)-1] == '=' { position = position[:len(position)-1] } return position } func parseMoves(moves []byte) ([4][2]int8, error) { var c int // Input cursor. var m [4][2]int8 // Parsed moves. var mc int // Parsed moves cursor. // Parse moves separated by spaces. l := len(moves) for c < l { var moveLength int for i := c; i < l; i++ { if moves[i] == ' ' { break } moveLength++ } if moveLength == 0 { return [4][2]int8{}, fmt.Errorf("failed to parse gnubg moves: no moves: %s", moves) } move := moves[c : c+moveLength] move = bytes.ReplaceAll(move, []byte("*"), nil) count := 1 paren := bytes.IndexRune(move, '(') if paren != -1 { var err error count, err = strconv.Atoi(string(move[paren+1])) if err != nil { return [4][2]int8{}, fmt.Errorf("failed to parse gnubg moves: invalid count: %s", moves) } mv := append(move[:paren], move[paren+3:]...) move = mv } slash := bytes.IndexRune(move, '/') if slash == -1 { return [4][2]int8{}, fmt.Errorf("failed to parse gnubg moves: invalid count: %s", moves) } nextSlash := bytes.IndexRune(move[slash+1:], '/') var remaining []byte if nextSlash != -1 { remaining = move[slash+nextSlash+2:] move = move[:slash+nextSlash+1] } from := bgammon.ParseSpace(string(move[:slash])) if from < 0 || from > 27 { return [4][2]int8{}, fmt.Errorf("failed to parse gnubg moves: invalid from (%s): %s", move[:slash], moves) } to := bgammon.ParseSpace(string(move[slash+1:])) if to < 0 || to > 27 { return [4][2]int8{}, fmt.Errorf("failed to parse gnubg moves: invalid to (%s): %s", move[:slash], moves) } for i := 0; i < count; i++ { m[mc][0], m[mc][1] = int8(from), int8(to) ok := Game.AddLocalMove([]int8{int8(from), int8(to)}) if !ok { return [4][2]int8{}, fmt.Errorf("failed to add local moves (%d/%d): %s", from, to, moves) } mc++ if len(remaining) > 0 { rm, err := parseMoves(append([]byte(strconv.Itoa(int(to))+"/"), remaining...)) if err != nil { return [4][2]int8{}, fmt.Errorf("failed to parse gnubg moves: invalid remaining moves (%s): %s", append([]byte(strconv.Itoa(int(to))+"/"), remaining...), moves) } for j := 0; j < 4; j++ { if rm[j][0] == 0 && rm[j][1] == 0 { break } m[mc][0], m[mc][1] = int8(rm[j][0]), int8(rm[j][1]) mc++ } } } // Advance to the next move. c += moveLength + 1 } // Relocate bear-off moves after moves within the board. var mb, mo [][2]int8 for j := 0; j < 4; j++ { if m[j][0] == 0 && m[j][1] == 0 { break } else if m[j][1] == bgammon.SpaceHomePlayer { mo = append(mo, m[j]) continue } mb = append(mb, m[j]) } m = [4][2]int8{} mc = 0 for _, mv := range mb { m[mc] = mv mc++ } for _, mv := range mo { m[mc] = mv mc++ } return m, nil } // analyze returns the best moves provided by the gnubg engine. func analyze(g *bgammon.Game) ([4][2]int8, error) { cmd := exec.Command("gnubg", "--tty", "--quiet", "--no-rc") stdin, err := cmd.StdinPipe() if err != nil { log.Fatal(err) } stdout, err := cmd.StdoutPipe() if err != nil { log.Fatal(err) } stderr, err := cmd.StderrPipe() if err != nil { log.Fatal(err) } err = cmd.Start() if err != nil { log.Fatal(err) } var moves []byte var ready bool go func() { scanner := bufio.NewScanner(stdout) for scanner.Scan() { buf := scanner.Bytes() if bytes.Contains(buf, []byte("The dice have been set")) || bytes.Contains(buf, []byte("The cube has been set")) { ready = true } else if ready && moves == nil && (bytes.Contains(buf, []byte("Rolled")) || bytes.Contains(buf, []byte("gnubg moves")) || bytes.Contains(buf, []byte("gnubg doubles")) || bytes.Contains(buf, []byte("gnubg accepts")) || bytes.Contains(buf, []byte("gnubg refuses the cube")) || bytes.Contains(buf, []byte("gnubg offers to resign"))) { moves = make([]byte, len(buf)) copy(moves, buf) } log.Println(string(buf)) } }() go func() { scanner := bufio.NewScanner(stderr) for scanner.Scan() { log.Println("!!!", string(scanner.Bytes())) } }() var extra []string if g.DoublePlayer != 0 { extra = append(extra, fmt.Sprintf("set cube owner %d", g.DoublePlayer-1)) } if g.DoubleValue != 0 { extra = append(extra, fmt.Sprintf("set cube value %d", g.DoubleValue)) } if g.DoubleOffered { extra = append(extra, "double") } else if g.Roll1 != 0 && g.Roll2 != 0 { extra = append(extra, fmt.Sprintf("set dice %d %d", g.Roll1, g.Roll2)) extra = append(extra, "play") } else { extra = append(extra, "play") } stdin.Write([]byte(fmt.Sprintf("set automatic roll off\nset automatic move off\nnew game\nset beavers 0\nset score %d %d %d\nset board %s\nset turn %d\n%s\n", g.Player1.Points, g.Player2.Points, g.Points, gnubgPosition(g), g.Turn-1, strings.Join(extra, "\n")))) t := time.NewTicker(50 * time.Millisecond) for { <-t.C if moves != nil { stdin.Write([]byte("quit\ny\n")) t.Stop() break } } err = cmd.Wait() if err != nil { return [4][2]int8{}, err } acceptCube := bytes.Contains(moves, []byte("gnubg accepts")) if acceptCube { return [4][2]int8{{-1, -1}}, nil } double := bytes.Contains(moves, []byte("gnubg doubles")) if double { return [4][2]int8{{-2, -2}}, nil } rollDice := bytes.Contains(moves, []byte("Rolled")) if rollDice { return [4][2]int8{{-3, -3}}, nil } resign := bytes.Contains(moves, []byte("gnubg offers to resign")) || bytes.Contains(moves, []byte("gnubg refuses the cube")) if resign { return [4][2]int8{{-4, -4}}, nil } movesWord := bytes.Index(moves, []byte("moves")) if movesWord == -1 { return [4][2]int8{}, fmt.Errorf("failed to parse gnubg moves: %s", moves) } moves = bytes.TrimSpace(moves[movesWord+6:]) if !bytes.HasSuffix(moves, []byte(".")) { return [4][2]int8{}, fmt.Errorf("failed to parse gnubg moves: %s", moves) } moves = moves[:len(moves)-1] return parseMoves(moves) } func FlipSpace(space int8, player int8, variant int8) int8 { if player == 1 { return space } if space < 1 || space > 24 { switch space { case bgammon.SpaceHomePlayer: return bgammon.SpaceHomeOpponent case bgammon.SpaceHomeOpponent: return bgammon.SpaceHomePlayer case bgammon.SpaceBarPlayer: return bgammon.SpaceBarOpponent case bgammon.SpaceBarOpponent: return bgammon.SpaceBarPlayer default: return -1 } } if variant == bgammon.VariantTabula { return space } return 24 - space + 1 } func FlipMoves(moves [4][2]int8, player int8, variant int8) [4][2]int8 { var m [4][2]int8 for i := 0; i < 4; i++ { if moves[i][0] == 0 && moves[i][1] == 0 { break } m[i][0], m[i][1] = FlipSpace(moves[i][0], player, variant), FlipSpace(moves[i][1], player, variant) } return m }