2024-11-17 05:42:56 +00:00
package main
import (
"bufio"
"bytes"
"encoding/base64"
"fmt"
"log"
"os/exec"
"strconv"
2024-11-26 20:57:28 +00:00
"strings"
2024-11-17 06:53:09 +00:00
"time"
2024-11-17 05:42:56 +00:00
"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
}
2024-11-19 03:49:00 +00:00
func parseMoves ( moves [ ] byte ) ( [ 4 ] [ 2 ] int8 , error ) {
2024-11-29 04:47:01 +00:00
var c int // Input cursor.
var m [ 4 ] [ 2 ] int8 // Parsed moves.
var mc int // Parsed moves cursor.
2024-11-17 05:42:56 +00:00
2024-11-29 04:47:01 +00:00
// Parse moves separated by spaces.
l := len ( moves )
for c < l {
2024-11-17 05:42:56 +00:00
var moveLength int
2024-11-29 04:47:01 +00:00
for i := c ; i < l ; i ++ {
if moves [ i ] == ' ' {
2024-11-17 05:42:56 +00:00
break
}
moveLength ++
}
if moveLength == 0 {
return [ 4 ] [ 2 ] int8 { } , fmt . Errorf ( "failed to parse gnubg moves: no moves: %s" , moves )
}
2024-11-29 04:47:01 +00:00
move := moves [ c : c + moveLength ]
2024-11-17 05:42:56 +00:00
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
}
2024-11-29 04:47:01 +00:00
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 ]
}
2024-11-17 05:42:56 +00:00
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 )
}
2024-11-29 04:47:01 +00:00
for i := 0 ; i < count ; i ++ {
2024-11-17 05:42:56 +00:00
m [ mc ] [ 0 ] , m [ mc ] [ 1 ] = int8 ( from ) , int8 ( to )
2024-11-17 06:53:09 +00:00
ok := Game . AddLocalMove ( [ ] int8 { int8 ( from ) , int8 ( to ) } )
2024-11-17 06:40:06 +00:00
if ! ok {
return [ 4 ] [ 2 ] int8 { } , fmt . Errorf ( "failed to add local moves (%d/%d): %s" , from , to , moves )
}
2024-11-17 05:42:56 +00:00
mc ++
2024-11-29 04:47:01 +00:00
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 ++
2024-11-17 05:42:56 +00:00
}
}
}
2024-11-29 04:47:01 +00:00
// Advance to the next move.
c += moveLength + 1
2024-11-17 05:42:56 +00:00
}
2024-11-29 04:47:01 +00:00
2024-11-19 03:49:00 +00:00
// 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 ++
}
2024-11-17 05:42:56 +00:00
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
2024-11-27 21:01:42 +00:00
var ready bool
2024-11-17 05:42:56 +00:00
go func ( ) {
scanner := bufio . NewScanner ( stdout )
for scanner . Scan ( ) {
buf := scanner . Bytes ( )
2024-11-26 20:57:28 +00:00
if bytes . Contains ( buf , [ ] byte ( "The dice have been set" ) ) || bytes . Contains ( buf , [ ] byte ( "The cube has been set" ) ) {
2024-11-27 21:01:42 +00:00
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 )
2024-11-17 05:42:56 +00:00
}
log . Println ( string ( buf ) )
}
} ( )
go func ( ) {
scanner := bufio . NewScanner ( stderr )
for scanner . Scan ( ) {
log . Println ( "!!!" , string ( scanner . Bytes ( ) ) )
}
} ( )
2024-11-26 20:57:28 +00:00
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" ) ) ) )
2024-11-17 06:53:09 +00:00
t := time . NewTicker ( 50 * time . Millisecond )
for {
<- t . C
if moves != nil {
stdin . Write ( [ ] byte ( "quit\ny\n" ) )
t . Stop ( )
break
}
}
2024-11-17 05:42:56 +00:00
err = cmd . Wait ( )
if err != nil {
return [ 4 ] [ 2 ] int8 { } , err
}
2024-11-26 20:57:28 +00:00
acceptCube := bytes . Contains ( moves , [ ] byte ( "gnubg accepts" ) )
if acceptCube {
2024-11-17 18:23:41 +00:00
return [ 4 ] [ 2 ] int8 { { - 1 , - 1 } } , nil
}
2024-11-26 20:57:28 +00:00
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
}
2024-11-17 06:28:38 +00:00
movesWord := bytes . Index ( moves , [ ] byte ( "moves" ) )
if movesWord == - 1 {
2024-11-17 05:42:56 +00:00
return [ 4 ] [ 2 ] int8 { } , fmt . Errorf ( "failed to parse gnubg moves: %s" , moves )
}
2024-11-17 06:28:38 +00:00
moves = bytes . TrimSpace ( moves [ movesWord + 6 : ] )
2024-11-17 05:42:56 +00:00
if ! bytes . HasSuffix ( moves , [ ] byte ( "." ) ) {
return [ 4 ] [ 2 ] int8 { } , fmt . Errorf ( "failed to parse gnubg moves: %s" , moves )
}
moves = moves [ : len ( moves ) - 1 ]
2024-11-19 03:49:00 +00:00
return parseMoves ( moves )
2024-11-17 05:42:56 +00:00
}
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
}