bgammon-gnubg-bot/gnubg.go

345 lines
8.6 KiB
Go

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
}