345 lines
8.6 KiB
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
|
|
}
|