tabula/board.go

618 lines
19 KiB
Go

package tabula
import (
"log"
"math"
"sort"
"sync"
)
var (
WeightBlot = 1.0
WeightHit = -1.0
WeightOppScore = -0.5
)
const (
SpaceHomePlayer = 0
SpaceHomeOpponent = 25
SpaceBarPlayer = 26
SpaceBarOpponent = 27
SpaceRoll1 = 28
SpaceRoll2 = 29
SpaceRoll3 = 30
SpaceRoll4 = 31
)
const (
boardSpaces = 32
)
type probabilityTable struct {
Roll1 int
Roll2 int
Chance float64
}
var rollProbabilities = []*probabilityTable{
{1, 1, 1.0},
{1, 2, 2.0},
{1, 3, 2.0},
{1, 4, 2.0},
{1, 5, 2.0},
{1, 6, 2.0},
{2, 2, 1.0},
{2, 3, 2.0},
{2, 4, 2.0},
{2, 5, 2.0},
{2, 6, 2.0},
{3, 3, 1.0},
{3, 4, 2.0},
{3, 5, 2.0},
{3, 6, 2.0},
{4, 4, 1.0},
{4, 5, 2.0},
{4, 6, 2.0},
{5, 5, 1.0},
{5, 6, 2.0},
{6, 6, 1.0},
}
// Board represents the state of a game. It contains spaces for the checkers,
// as well as four "spaces" which contain the available die rolls.
type Board [boardSpaces]int8
// NewBoard returns a new board with checkers placed in their starting positions.
func NewBoard() Board {
return Board{0, -2, 0, 0, 0, 0, 5, 0, 3, 0, 0, 0, -5, 5, 0, 0, 0, -3, 0, -5, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0}
}
func (b Board) SetValue(space int, value int8) Board {
b[space] = value
return b
}
// Move moves a checker on the board.
func (b Board) Move(from int, to int, player int) Board {
if b[from] == 0 || (player == 1 && b[from] < 0) || (player == 2 && b[from] > 0) {
log.Panic("illegal move: no from checker", from, to, player)
} else if b[to] != 0 {
if (player == 1 && b[to] == -1) || (player == 2 && b[to] == 1) {
b[to] = 0
if player == 1 {
b[SpaceBarOpponent] -= 1
} else {
b[SpaceBarPlayer] += 1
}
} else if (player == 1 && b[to] < 0) || (player == 2 && b[to] > 0) {
b.Print()
log.Panic("illegal move: existing checkers at to space", from, to, player, b[to])
}
}
delta := int8(1)
if player == 2 {
delta = int8(-1)
}
b[from], b[to] = b[from]-delta, b[to]+delta
return b
}
// Checkers returns the number of checkers at a space. It always returns a positive number.
func (b Board) Checkers(space int, player int) int8 {
v := b[space]
if player == 1 && v > 0 {
return v
} else if player == 2 && v < 0 {
return v * -1
}
return 0
}
func (b Board) MayBearOff(player int) bool {
homeStart := 1
homeEnd := 6
barSpace := SpaceBarPlayer
if player == 2 {
homeStart = 19
homeEnd = 24
barSpace = SpaceBarOpponent
}
if b.Checkers(barSpace, player) != 0 {
return false
}
for space := 1; space < 25; space++ {
if space >= homeStart && space <= homeEnd {
continue
} else if b.Checkers(space, player) != 0 {
return false
}
}
return true
}
// HaveRoll returns whether the player has a sufficient die roll for the specified move.
func (b Board) HaveRoll(from int, to int, player int) bool {
delta := int8(spaceDiff(from, to))
if delta == 0 {
return false
}
playerDelta := -1
playerHomeEnd := 6
if player == 2 {
playerDelta = 1
playerHomeEnd = 19
}
if b.MayBearOff(player) {
allowGreater := true
for checkSpace := int8(0); checkSpace < 6-delta; checkSpace++ {
if b.Checkers(playerHomeEnd+int(checkSpace)*playerDelta, player) != 0 {
allowGreater = false
break
}
}
if allowGreater {
return (b[SpaceRoll1] >= delta || b[SpaceRoll2] >= delta || b[SpaceRoll3] >= delta || b[SpaceRoll4] >= delta)
}
}
return (b[SpaceRoll1] == delta || b[SpaceRoll2] == delta || b[SpaceRoll3] == delta || b[SpaceRoll4] == delta)
}
// UseRoll uses a die roll.
func (b Board) UseRoll(from int, to int, player int) Board {
delta := int8(spaceDiff(from, to))
if delta == 0 {
b.Print()
log.Panic("unknown space diff", from, to, player)
}
playerDelta := -1
playerHomeEnd := 6
if player == 2 {
playerDelta = 1
playerHomeEnd = 19
}
var allowGreater bool
if b.MayBearOff(player) {
allowGreater = true
for checkSpace := int8(0); checkSpace < 6-delta; checkSpace++ {
if b.Checkers(playerHomeEnd+int(checkSpace)*playerDelta, player) != 0 {
allowGreater = false
break
}
}
}
if allowGreater {
switch {
case b[SpaceRoll1] >= delta:
b[SpaceRoll1] = 0
case b[SpaceRoll2] >= delta:
b[SpaceRoll2] = 0
case b[SpaceRoll3] >= delta:
b[SpaceRoll3] = 0
case b[SpaceRoll4] >= delta:
b[SpaceRoll4] = 0
default:
b.Print()
log.Panic("no available roll for move", from, to, player)
}
} else {
switch {
case b[SpaceRoll1] == delta:
b[SpaceRoll1] = 0
case b[SpaceRoll2] == delta:
b[SpaceRoll2] = 0
case b[SpaceRoll3] == delta:
b[SpaceRoll3] = 0
case b[SpaceRoll4] == delta:
b[SpaceRoll4] = 0
default:
b.Print()
log.Panic("no available roll for move", from, to, player)
}
}
return b
}
// Available returns legal moves available.
func (b Board) Available(player int) [][]int {
barSpace := SpaceBarPlayer
opponentBarSpace := SpaceBarOpponent
if player == 2 {
barSpace = SpaceBarOpponent
opponentBarSpace = SpaceBarPlayer
}
mayBearOff := b.MayBearOff(player)
onBar := b[barSpace] != 0
var moves [][]int
for from := 0; from < 28; from++ {
if from == SpaceHomePlayer || from == SpaceHomeOpponent || from == opponentBarSpace || b.Checkers(from, player) == 0 || (onBar && from != barSpace) {
continue
}
if player == 1 {
for to := 0; to < from; to++ {
if to == SpaceBarPlayer || to == SpaceBarOpponent || to == SpaceHomeOpponent || (to == SpaceHomePlayer && !mayBearOff) {
continue
}
v := b[to]
if (player == 1 && v < -1) || (player == 2 && v > 1) || !b.HaveRoll(from, to, player) {
continue
}
moves = append(moves, []int{from, to})
}
} else { // TODO clean up
start := from + 1
if from == SpaceBarOpponent {
start = 0
}
for to := start; to <= 25; to++ {
if to == SpaceBarPlayer || to == SpaceBarOpponent || to == SpaceHomeOpponent || (to == SpaceHomeOpponent && !mayBearOff) {
continue
}
v := b[to]
if (player == 1 && v < -1) || (player == 2 && v > 1) || !b.HaveRoll(from, to, player) {
continue
}
moves = append(moves, []int{from, to})
}
}
}
return moves
}
func (b Board) Past(player int) bool {
if b[SpaceBarPlayer] != 0 || b[SpaceBarOpponent] != 0 {
return false
}
var playerFirst, opponentLast int
for space := 1; space < 25; space++ {
v := b[space]
if v == 0 {
continue
} else if v > 0 {
if player == 1 && playerFirst == 0 {
playerFirst = space
if opponentLast != 0 {
break
}
} else if player == 2 && opponentLast == 0 {
opponentLast = space
if playerFirst != 0 {
break
}
}
} else {
if player == 1 && opponentLast == 0 {
opponentLast = space
if playerFirst != 0 {
break
}
} else if player == 2 && playerFirst == 0 {
playerFirst = space
if opponentLast != 0 {
break
}
}
}
}
if player == 1 {
return playerFirst < opponentLast
} else {
return playerFirst > opponentLast
}
}
func (b Board) spaceValue(player int, space int) int {
var spaceValue int
if player == 1 {
spaceValue = space*2 + 7
if space > 6 {
spaceValue += 7
}
} else {
spaceValue = (25-space)*2 + 7
if space < 19 {
spaceValue += 7
}
}
return spaceValue
}
func (b Board) Pips(player int) int {
var pips int
if player == 1 {
pips += int(b.Checkers(SpaceBarPlayer, player))*50 + 14
} else {
pips += int(b.Checkers(SpaceBarOpponent, player))*50 + 14
}
for space := 1; space < 25; space++ {
pips += int(b.Checkers(space, player)) * b.spaceValue(space, player)
}
return pips
}
func (b Board) Blots(player int) int {
var pips int
for space := 1; space < 25; space++ {
checkers := b.Checkers(space, player)
if checkers != 1 {
continue
}
pips += int(checkers) * b.spaceValue(space, player)
}
return pips
}
func (b Board) Score(player int, hitScore int) float64 {
pips := b.Pips(player)
blots := b.Blots(player)
return float64(pips) + float64(blots)*WeightBlot + float64(hitScore)*WeightHit
}
func (b Board) evaluate(player int, hitScore int, a *Analysis) {
pips := b.Pips(player)
score := float64(pips)
var blots int
if !a.Past {
blots := b.Blots(player)
score += float64(blots)*WeightBlot + float64(hitScore)*WeightHit
}
a.Pips = pips
a.Blots = blots
a.Hits = hitScore
a.PlayerScore = score
a.hitScore = hitScore
}
func (b Board) Evaluation(player int, hitScore int, moves [][]int) *Analysis {
past := b.Past(player)
a := &Analysis{
Board: b,
Moves: moves,
Past: past,
}
b.evaluate(player, hitScore, a)
return a
}
func queueAnalysis(a *Analysis, w *sync.WaitGroup, b Board, player int, available [][]int, moves [][]int, found *[][][]int, result *[]*Analysis, resultMutex *sync.Mutex) {
for _, move := range available {
move := move
go func() {
newMoves := append(append([][]int{}, moves...), move)
resultMutex.Lock()
for _, f := range *found {
if movesEqual(f, newMoves) {
resultMutex.Unlock()
w.Done()
return
}
}
*found = append(*found, newMoves)
resultMutex.Unlock()
checkers := b.Checkers(move[1], opponent(player))
hs := a.hitScore
if checkers == 1 {
hs += b.spaceValue(player, move[1])
}
a := &Analysis{
Board: b.Move(move[0], move[1], player).UseRoll(move[0], move[1], player),
Moves: newMoves,
Past: a.Past,
}
a.Board.evaluate(player, hs, a)
available := a.Board.Available(player)
w.Add(len(available))
queueAnalysis(a, w, a.Board, player, available, a.Moves, found, result, resultMutex)
resultMutex.Lock()
*result = append(*result, a)
resultMutex.Unlock()
w.Done()
}()
}
}
func (b Board) Analyze(player int, available [][]int) []*Analysis {
if len(available) == 0 {
return nil
}
const bufferSize = 128
var found [][][]int
w := &sync.WaitGroup{}
result := make([]*Analysis, 0, bufferSize)
resultMutex := &sync.Mutex{}
a := &Analysis{
Past: b.Past(player),
}
b.evaluate(player, 0, a)
w.Add(len(available))
queueAnalysis(a, w, b, player, available, nil, &found, &result, resultMutex)
w.Wait()
var maxMoves int
for i := range result {
l := len(result[i].Moves)
if l > maxMoves {
maxMoves = l
}
}
var newResult []*Analysis
for i := 0; i < len(result); i++ {
if len(result[i].Moves) == maxMoves {
newResult = append(newResult, result[i])
}
}
result = newResult
if player == 1 && !a.Past {
oppResults := make([][]*Analysis, len(result))
w.Add(len(result) * 21)
for i := range result {
i := i
oppResultMutex := &sync.Mutex{}
oppResults[i] = make([]*Analysis, 0, bufferSize)
for j := 0; j < 21; j++ {
j := j
go func() {
check := rollProbabilities[j]
bc := Board{}
bc = result[i].Board
bc[SpaceRoll1], bc[SpaceRoll2] = int8(check.Roll1), int8(check.Roll2)
if int8(check.Roll1) == int8(check.Roll2) {
bc[SpaceRoll3], bc[SpaceRoll4] = int8(check.Roll1), int8(check.Roll2)
}
available := bc.Available(2)
a := &Analysis{
Past: b.Past(2),
}
w.Add(len(available))
queueAnalysis(a, w, bc, 2, available, nil, &[][][]int{}, &oppResults[i], oppResultMutex)
w.Done()
}()
}
}
w.Wait()
for i := range result {
var oppPips float64
var oppBlots float64
var oppHits float64
var oppScore float64
var count float64
for _, r := range oppResults[i] {
oppPips += float64(r.Pips)
oppBlots += float64(r.Blots)
oppHits += float64(r.Hits)
oppScore += r.PlayerScore
count++
}
result[i].OppPips = (oppPips / count)
result[i].OppBlots = (oppBlots / count)
result[i].OppHits = (oppHits / count)
result[i].OppScore = (oppScore / count)
score := result[i].PlayerScore
if !math.IsNaN(oppScore) {
score += result[i].OppScore * WeightOppScore
}
result[i].Score = score
}
} else {
for i := range result {
result[i].Score = result[i].PlayerScore
}
}
sort.Slice(result, func(i, j int) bool {
return result[i].Score < result[j].Score
})
return result
}
func (b Board) Print() {
log.Printf("%+v", b)
}
func opponent(player int) int {
if player == 1 {
return 2
} else {
return 1
}
}
func spaceDiff(from int, to int) int {
if from < 0 || from > 27 || to < 0 || to > 27 {
return 0
} else if to == SpaceBarPlayer || to == SpaceBarOpponent {
return 0
} else if from == SpaceHomePlayer || from == SpaceHomeOpponent {
return 0
}
if (from == SpaceBarPlayer || from == SpaceBarOpponent) && (to == SpaceBarPlayer || to == SpaceBarOpponent || to == SpaceHomePlayer || to == SpaceHomeOpponent) {
return 0
}
if from == SpaceBarPlayer {
return 25 - to
} else if from == SpaceBarOpponent {
return to
}
if to == SpaceHomePlayer {
return from
} else if to == SpaceHomeOpponent {
return 25 - from
}
diff := to - from
if diff < 0 {
return diff * -1
}
return diff
}
func movesEqual(a [][]int, b [][]int) bool {
l := len(a)
if len(b) != l {
return false
}
for _, m := range a {
switch m[0] {
case SpaceBarPlayer, SpaceBarOpponent:
return false
}
switch m[1] {
case SpaceHomePlayer, SpaceHomeOpponent:
return false
}
}
switch l {
case 0:
return true
case 1:
return a[0][0] == b[0][0] && a[0][1] == b[0][1]
case 2:
return (a[0][0] == b[0][0] && a[0][1] == b[0][1] && a[1][0] == b[1][0] && a[1][1] == b[1][1]) || // 1, 2
(a[0][0] == b[1][0] && a[0][1] == b[1][1] && a[1][0] == b[0][0] && a[1][1] == b[0][1]) // 2, 1
case 3:
return (a[0][0] == b[0][0] && a[0][1] == b[0][1] && a[1][0] == b[1][0] && a[1][1] == b[1][1] && a[2][0] == b[2][0] && a[2][1] == b[2][1]) || // 1, 2, 3
(a[0][0] == b[1][0] && a[0][1] == b[1][1] && a[1][0] == b[2][0] && a[1][1] == b[2][1] && a[2][0] == b[0][0] && a[2][1] == b[0][1]) || // 2, 3, 1
(a[0][0] == b[2][0] && a[0][1] == b[2][1] && a[1][0] == b[0][0] && a[1][1] == b[0][1] && a[2][0] == b[1][0] && a[2][1] == b[1][1]) || // 3, 1, 2
(a[0][0] == b[0][0] && a[0][1] == b[0][1] && a[1][0] == b[2][0] && a[1][1] == b[2][1] && a[2][0] == b[1][0] && a[2][1] == b[1][1]) || // 1, 3, 2
(a[0][0] == b[1][0] && a[0][1] == b[1][1] && a[1][0] == b[0][0] && a[1][1] == b[0][1] && a[2][0] == b[2][0] && a[2][1] == b[2][1]) || // 2, 1, 3
(a[0][0] == b[2][0] && a[0][1] == b[2][1] && a[1][0] == b[1][0] && a[1][1] == b[1][1] && a[2][0] == b[0][0] && a[2][1] == b[0][1]) // 3, 2, 1
case 4:
return (a[0][0] == b[0][0] && a[0][1] == b[0][1] && a[1][0] == b[1][0] && a[1][1] == b[1][1] && a[2][0] == b[2][0] && a[2][1] == b[2][1] && a[3][0] == b[3][0] && a[3][1] == b[3][1]) || // 1,2,3,4
(a[0][0] == b[1][0] && a[0][1] == b[1][1] && a[1][0] == b[0][0] && a[1][1] == b[0][1] && a[2][0] == b[2][0] && a[2][1] == b[2][1] && a[3][0] == b[3][0] && a[3][1] == b[3][1]) || // 2,1,3,4
(a[0][0] == b[2][0] && a[0][1] == b[2][1] && a[1][0] == b[0][0] && a[1][1] == b[0][1] && a[2][0] == b[1][0] && a[2][1] == b[1][1] && a[3][0] == b[3][0] && a[3][1] == b[3][1]) || // 3,1,2,4
(a[0][0] == b[0][0] && a[0][1] == b[0][1] && a[1][0] == b[2][0] && a[1][1] == b[2][1] && a[2][0] == b[1][0] && a[2][1] == b[1][1] && a[3][0] == b[3][0] && a[3][1] == b[3][1]) || // 1,3,2,4
(a[0][0] == b[1][0] && a[0][1] == b[1][1] && a[1][0] == b[2][0] && a[1][1] == b[2][1] && a[2][0] == b[0][0] && a[2][1] == b[0][1] && a[3][0] == b[3][0] && a[3][1] == b[3][1]) || // 2,3,1,4
(a[0][0] == b[2][0] && a[0][1] == b[2][1] && a[1][0] == b[1][0] && a[1][1] == b[1][1] && a[2][0] == b[0][0] && a[2][1] == b[0][1] && a[3][0] == b[3][0] && a[3][1] == b[3][1]) || // 3,2,1,4
(a[0][0] == b[2][0] && a[0][1] == b[2][1] && a[1][0] == b[1][0] && a[1][1] == b[1][1] && a[2][0] == b[3][0] && a[2][1] == b[3][1] && a[3][0] == b[0][0] && a[3][1] == b[0][1]) || // 3,2,4,1
(a[0][0] == b[1][0] && a[0][1] == b[1][1] && a[1][0] == b[2][0] && a[1][1] == b[2][1] && a[2][0] == b[3][0] && a[2][1] == b[3][1] && a[3][0] == b[0][0] && a[3][1] == b[0][1]) || // 2,3,4,1
(a[0][0] == b[3][0] && a[0][1] == b[3][1] && a[1][0] == b[2][0] && a[1][1] == b[2][1] && a[2][0] == b[1][0] && a[2][1] == b[1][1] && a[3][0] == b[0][0] && a[3][1] == b[0][1]) || // 4,3,2,1
(a[0][0] == b[2][0] && a[0][1] == b[2][1] && a[1][0] == b[3][0] && a[1][1] == b[3][1] && a[2][0] == b[1][0] && a[2][1] == b[1][1] && a[3][0] == b[0][0] && a[3][1] == b[0][1]) || // 3,4,2,1
(a[0][0] == b[1][0] && a[0][1] == b[1][1] && a[1][0] == b[3][0] && a[1][1] == b[3][1] && a[2][0] == b[2][0] && a[2][1] == b[2][1] && a[3][0] == b[0][0] && a[3][1] == b[0][1]) || // 2,4,3,1
(a[0][0] == b[3][0] && a[0][1] == b[3][1] && a[1][0] == b[1][0] && a[1][1] == b[1][1] && a[2][0] == b[2][0] && a[2][1] == b[2][1] && a[3][0] == b[0][0] && a[3][1] == b[0][1]) || // 4,2,3,1
(a[0][0] == b[3][0] && a[0][1] == b[3][1] && a[1][0] == b[0][0] && a[1][1] == b[0][1] && a[2][0] == b[2][0] && a[2][1] == b[2][1] && a[3][0] == b[1][0] && a[3][1] == b[1][1]) || // 4,1,3,2
(a[0][0] == b[0][0] && a[0][1] == b[0][1] && a[1][0] == b[3][0] && a[1][1] == b[3][1] && a[2][0] == b[2][0] && a[2][1] == b[2][1] && a[3][0] == b[1][0] && a[3][1] == b[1][1]) || // 1,4,3,2
(a[0][0] == b[2][0] && a[0][1] == b[2][1] && a[1][0] == b[3][0] && a[1][1] == b[3][1] && a[2][0] == b[0][0] && a[2][1] == b[0][1] && a[3][0] == b[1][0] && a[3][1] == b[1][1]) || // 3,4,1,2
(a[0][0] == b[3][0] && a[0][1] == b[3][1] && a[1][0] == b[2][0] && a[1][1] == b[2][1] && a[2][0] == b[0][0] && a[2][1] == b[0][1] && a[3][0] == b[1][0] && a[3][1] == b[1][1]) || // 4,3,1,2
(a[0][0] == b[0][0] && a[0][1] == b[0][1] && a[1][0] == b[2][0] && a[1][1] == b[2][1] && a[2][0] == b[3][0] && a[2][1] == b[3][1] && a[3][0] == b[1][0] && a[3][1] == b[1][1]) || // 1,3,4,2
(a[0][0] == b[2][0] && a[0][1] == b[2][1] && a[1][0] == b[0][0] && a[1][1] == b[0][1] && a[2][0] == b[3][0] && a[2][1] == b[3][1] && a[3][0] == b[1][0] && a[3][1] == b[1][1]) || // 3,1,4,2
(a[0][0] == b[1][0] && a[0][1] == b[1][1] && a[1][0] == b[0][0] && a[1][1] == b[0][1] && a[2][0] == b[3][0] && a[2][1] == b[3][1] && a[3][0] == b[2][0] && a[3][1] == b[2][1]) || // 2,1,4,3
(a[0][0] == b[0][0] && a[0][1] == b[0][1] && a[1][0] == b[1][0] && a[1][1] == b[1][1] && a[2][0] == b[3][0] && a[2][1] == b[3][1] && a[3][0] == b[2][0] && a[3][1] == b[2][1]) || // 1,2,4,3
(a[0][0] == b[3][0] && a[0][1] == b[3][1] && a[1][0] == b[1][0] && a[1][1] == b[1][1] && a[2][0] == b[0][0] && a[2][1] == b[0][1] && a[3][0] == b[2][0] && a[3][1] == b[2][1]) || // 4,2,1,3
(a[0][0] == b[1][0] && a[0][1] == b[1][1] && a[1][0] == b[3][0] && a[1][1] == b[3][1] && a[2][0] == b[0][0] && a[2][1] == b[0][1] && a[3][0] == b[2][0] && a[3][1] == b[2][1]) || // 2,4,1,3
(a[0][0] == b[0][0] && a[0][1] == b[0][1] && a[1][0] == b[3][0] && a[1][1] == b[3][1] && a[2][0] == b[1][0] && a[2][1] == b[1][1] && a[3][0] == b[2][0] && a[3][1] == b[2][1]) || // 1,4,2,3
(a[0][0] == b[3][0] && a[0][1] == b[3][1] && a[1][0] == b[0][0] && a[1][1] == b[0][1] && a[2][0] == b[1][0] && a[2][1] == b[1][1] && a[3][0] == b[2][0] && a[3][1] == b[2][1]) // 4,1,2,3
default:
log.Panicf("more than 4 moves were provided: %+v %+v", a, b)
return false
}
}