package tabula import ( "log" "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} } 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) Pips(player int) int { var pips float64 var spaceValue float64 if player == 1 { pips += float64(b.Checkers(SpaceBarPlayer, player)) * 25 } else { pips += float64(b.Checkers(SpaceBarOpponent, player)) * 25 } for space := 1; space < 25; space++ { if player == 1 { spaceValue = float64(space) if space <= 6 { spaceValue /= 4 } else { spaceValue += 6 } } else { spaceValue = float64(25 - space) if space >= 19 { spaceValue /= 4 } else { spaceValue += 6 } } pips += float64(b.Checkers(space, player)) * spaceValue } return int(pips) } func (b Board) Blots(player int) int { var pips int var spaceValue int for space := 1; space < 25; space++ { checkers := b.Checkers(space, player) if checkers != 1 { continue } if player == 1 { spaceValue = 25 - space } else { spaceValue = space } pips += int(checkers) * spaceValue } 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) blots := b.Blots(player) score := float64(pips) + float64(blots)*WeightBlot + float64(hitScore)*WeightHit a.Pips = pips a.Blots = blots a.Hits = hitScore a.PlayerScore = score } func (b Board) Evaluation(player int, hitScore int, moves [][]int) *Analysis { pips := b.Pips(player) blots := b.Blots(player) score := float64(pips) + float64(blots)*WeightBlot + float64(hitScore)*WeightHit return &Analysis{ Board: b, Moves: moves, Pips: pips, Blots: blots, Hits: hitScore, PlayerScore: score, } } func queueAnalysis(a *Analysis, w *sync.WaitGroup, b Board, player int, available [][]int, moves [][]int, found *[][][]int, result *[]*Analysis, resultMutex *sync.Mutex) { var hs int resultMutex.Lock() QUEUE: for _, move := range available { move := move newMoves := append(append([][]int{}, moves...), move) for _, f := range *found { if movesEqual(f, newMoves) { continue QUEUE } } *found = append(*found, newMoves) w.Add(1) go func() { checkers := b.Checkers(move[1], opponent(player)) hs = 0 if checkers == 1 { if player == 1 { hs = move[1] } else { hs = 25 - move[1] } } a := &Analysis{ Board: b.Move(move[0], move[1], player).UseRoll(move[0], move[1], player), Moves: newMoves, player: player, hitScore: hs, } a.Board.evaluate(player, hs, a) queueAnalysis(a, w, a.Board, player, a.Board.Available(player), a.Moves, found, result, resultMutex) resultMutex.Lock() *result = append(*result, a) resultMutex.Unlock() w.Done() }() } resultMutex.Unlock() } 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{} b.evaluate(player, 0, a) 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 { oppResults := make([][]*Analysis, len(result)) for i := range result { i := i oppResultMutex := &sync.Mutex{} oppResults[i] = make([]*Analysis, 0, bufferSize) for j := 0; j < 21; j++ { j := j 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) } queueAnalysis(a, w, bc, 2, bc.Available(2), nil, &[][][]int{}, &oppResults[i], oppResultMutex) } } 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) result[i].Score = result[i].PlayerScore + result[i].OppScore*WeightOppScore } } 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 } }