diff --git a/DESIGN.md b/DESIGN.md index 033c71a..57977e8 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -30,8 +30,8 @@ Space value is defined as what the term 'pips' would normally refer to for a given space. The value of a space is the same as the space number as it appears to the player being scored. -Base value is 12 for spaces within the home board of the player being scored. -All other spaces have a base value of 36. The base values incentivize +Base value is 6 for spaces within the home board of the player being scored. +All other spaces have a base value of 42. The base values incentivize prioritizing moving all checkers into the player's home board, and subsequently bearing checkers off instead of moving them. diff --git a/analysis.go b/analysis.go index f50ed45..7f70ecc 100644 --- a/analysis.go +++ b/analysis.go @@ -2,8 +2,41 @@ package tabula import ( "fmt" + "math" + "sync" ) +var ( + WeightBlot = 1.03 + WeightHit = -0.5 + WeightOppScore = -3.0 +) + +// rollProbabilities is a table of the probability of each roll combination. +var rollProbabilities = [21][3]int{ + {1, 1, 1}, + {1, 2, 2}, + {1, 3, 2}, + {1, 4, 2}, + {1, 5, 2}, + {1, 6, 2}, + {2, 2, 1}, + {2, 3, 2}, + {2, 4, 2}, + {2, 5, 2}, + {2, 6, 2}, + {3, 3, 1}, + {3, 4, 2}, + {3, 5, 2}, + {3, 6, 2}, + {4, 4, 1}, + {4, 5, 2}, + {4, 6, 2}, + {5, 5, 1}, + {5, 6, 2}, + {6, 6, 1}, +} + type Analysis struct { Board Board Moves [][]int @@ -20,7 +53,108 @@ type Analysis struct { OppHits float64 OppScore float64 + player int hitScore int + chance int +} + +func (a *Analysis) _analyze(result *[]*Analysis, resultMutex *sync.Mutex, w *sync.WaitGroup) { + var hs int + o := opponent(a.player) + for i := 0; i < len(a.Moves); i++ { + move := a.Moves[i] + checkers := a.Board.Checkers(o, move[1]) + if checkers == 1 { + hs += pseudoPips(o, move[1]) + } + a.Board = a.Board.Move(move[0], move[1], a.player).UseRoll(move[0], move[1], a.player) + } + a.Board.evaluate(a.player, hs, a) + + if a.player == 1 && !a.Past { + const bufferSize = 1024 + oppResults := make([]*Analysis, 0, bufferSize) + oppResultMutex := &sync.Mutex{} + wg := &sync.WaitGroup{} + wg.Add(21) + for j := 0; j < 21; j++ { + j := j + go func() { + check := rollProbabilities[j] + bc := a.Board + bc[SpaceRoll1], bc[SpaceRoll2] = int8(check[0]), int8(check[1]) + if check[0] == check[1] { + bc[SpaceRoll3], bc[SpaceRoll4] = int8(check[0]), int8(check[1]) + } else { + bc[SpaceRoll3], bc[SpaceRoll4] = 0, 0 + } + available, _ := bc.Available(2) + if len(available) == 0 { + a := &Analysis{ + Board: bc, + Past: a.Past, + player: 2, + chance: check[2], + } + bc.evaluate(a.player, 0, a) + oppResultMutex.Lock() + for i := 0; i < check[2]; i++ { + oppResults = append(oppResults, a) + } + oppResultMutex.Unlock() + wg.Done() + return + } + wg.Add(len(available) - 1) + for _, moves := range available { + a := &Analysis{ + Board: bc, + Moves: moves, + Past: a.Past, + player: 2, + chance: check[2], + } + go a._analyze(&oppResults, oppResultMutex, wg) + } + }() + } + wg.Wait() + + var oppPips float64 + var oppBlots float64 + var oppHits float64 + var oppScore float64 + var count float64 + for _, r := range oppResults { + oppPips += float64(r.Pips) + oppBlots += float64(r.Blots) + oppHits += float64(r.Hits) + oppScore += r.PlayerScore + count++ + } + if count == 0 { + a.Score = a.PlayerScore + } else { + a.OppPips = (oppPips / count) + a.OppBlots = (oppBlots / count) + a.OppHits = (oppHits / count) + a.OppScore = (oppScore / count) + score := a.PlayerScore + if !math.IsNaN(oppScore) { + score += a.OppScore * WeightOppScore + } + a.Score = score + } + } else { + a.Score = a.PlayerScore + } + + resultMutex.Lock() + for i := 0; i < a.chance; i++ { + *result = append(*result, a) + } + resultMutex.Unlock() + w.Done() } func (a *Analysis) String() string { diff --git a/board.go b/board.go index 07f86ef..c0ef9ea 100644 --- a/board.go +++ b/board.go @@ -7,12 +7,6 @@ import ( "sync" ) -var ( - WeightBlot = 1.0 - WeightHit = -1.0 - WeightOppScore = -10.0 -) - const ( SpaceHomePlayer = 0 SpaceHomeOpponent = 25 @@ -28,36 +22,6 @@ 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 @@ -212,8 +176,7 @@ func (b Board) UseRoll(from int, to int, player int) Board { return b } -// Available returns legal moves available. -func (b Board) Available(player int) [][]int { +func (b Board) _available(player int) [][]int { barSpace := SpaceBarPlayer opponentBarSpace := SpaceBarOpponent if player == 2 { @@ -258,6 +221,82 @@ func (b Board) Available(player int) [][]int { return moves } +// Available returns legal moves available. +func (b Board) Available(player int) ([][][]int, []Board) { + var allMoves [][][]int + + resultMutex := &sync.Mutex{} + movesFound := func(moves [][]int) bool { + resultMutex.Lock() + for _, f := range allMoves { + if movesEqual(f, moves) { + resultMutex.Unlock() + return true + } + } + resultMutex.Unlock() + return false + } + + var boards []Board + a := b._available(player) + maxLen := 1 + for _, move := range a { + newBoard := b.Move(move[0], move[1], player).UseRoll(move[0], move[1], player) + newAvailable := newBoard._available(player) + if len(newAvailable) == 0 { + moves := [][]int{move} + if !movesFound(moves) { + allMoves = append(allMoves, moves) + boards = append(boards, newBoard) + } + continue + } + for _, move2 := range newAvailable { + newBoard2 := newBoard.Move(move2[0], move2[1], player).UseRoll(move2[0], move2[1], player) + newAvailable2 := newBoard2._available(player) + if len(newAvailable2) == 0 { + moves := [][]int{move, move2} + if !movesFound(moves) { + allMoves = append(allMoves, moves) + boards = append(boards, newBoard2) + maxLen = 2 + } + continue + } + for _, move3 := range newAvailable2 { + newBoard3 := newBoard2.Move(move3[0], move3[1], player).UseRoll(move3[0], move3[1], player) + newAvailable3 := newBoard3._available(player) + if len(newAvailable3) == 0 { + moves := [][]int{move, move2, move3} + if !movesFound(moves) { + allMoves = append(allMoves, moves) + boards = append(boards, newBoard3) + maxLen = 3 + } + continue + } + for _, move4 := range newAvailable3 { + newBoard4 := newBoard3.Move(move4[0], move4[1], player).UseRoll(move4[0], move4[1], player) + moves := [][]int{move, move2, move3, move4} + if !movesFound(moves) { + allMoves = append(allMoves, moves) + boards = append(boards, newBoard4) + maxLen = 4 + } + } + } + } + } + var newMoves [][][]int + for i := 0; i < len(allMoves); i++ { + if len(allMoves[i]) >= maxLen { + newMoves = append(newMoves, allMoves[i]) + } + } + return newMoves, boards +} + func (b Board) Past() bool { if b[SpaceBarPlayer] != 0 || b[SpaceBarOpponent] != 0 { return false @@ -323,149 +362,40 @@ func (b Board) evaluate(player int, hitScore int, a *Analysis) { func (b Board) Evaluation(player int, hitScore int, moves [][]int) *Analysis { a := &Analysis{ - Board: b, - Moves: moves, - Past: b.Past(), + Board: b, + Moves: moves, + Past: b.Past(), + player: player, + chance: 1, } 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) { - startingHitScore := a.hitScore - 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() - - o := opponent(player) - checkers := b.Checkers(o, move[1]) - hs := startingHitScore - if checkers == 1 { - hs += pseudoPips(o, 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 { +func (b Board) Analyze(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{} + w := &sync.WaitGroup{} - a := &Analysis{ - Past: b.Past(), - } - b.evaluate(player, 0, a) - + past := b.Past() w.Add(len(available)) - queueAnalysis(a, w, b, player, available, nil, &found, &result, resultMutex) + for _, moves := range available { + a := &Analysis{ + Board: b, + Moves: moves, + Past: past, + player: 1, + chance: 1, + } + go a._analyze(&result, resultMutex, w) + } 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 := 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: a.Past, - } - bc.evaluate(player, 0, a) - 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 }) @@ -494,9 +424,9 @@ func spaceValue(player int, space int) int { } func pseudoPips(player int, space int) int { - v := 12 + spaceValue(player, space) + int(math.Exp(float64(spaceValue(player, space))*0.2))*2 + v := 6 + spaceValue(player, space) + int(math.Exp(float64(spaceValue(player, space))*0.2))*2 if (player == 1 && (space > 6 || space == SpaceBarPlayer)) || (player == 2 && (space < 19 || space == SpaceBarOpponent)) { - v += 24 + v += 36 } return v } diff --git a/board_test.go b/board_test.go index 8d1e5e3..e737391 100644 --- a/board_test.go +++ b/board_test.go @@ -75,7 +75,7 @@ func TestBlots(t *testing.T) { b = b.Move(24, 23, 1) - got, expected = b.Blots(1), 31 + got, expected = b.Blots(1), 19 if got != expected { t.Errorf("unexpected blots value: expected %v: got %v", expected, got) } @@ -86,11 +86,11 @@ func TestBlots(t *testing.T) { b = b.Move(1, 2, 2) - got, expected = b.Blots(1), 31 + got, expected = b.Blots(1), 19 if got != expected { t.Errorf("unexpected blots value: expected %v: got %v", expected, got) } - got, expected = b.Blots(2), 31 + got, expected = b.Blots(2), 19 if got != expected { t.Errorf("unexpected blots value: expected %v: got %v", expected, got) } @@ -102,15 +102,14 @@ func TestAnalyze(t *testing.T) { b = b.Move(1, 2, 2) b[SpaceRoll1], b[SpaceRoll2] = 1, 2 - for player := 1; player <= 2; player++ { - r := b.Analyze(player, b.Available(player)) - var blots int - for _, r := range r { - blots += r.Blots - } - if blots <= 0 { - t.Errorf("expected >0 blots for player %d in results, got %d", player, blots) - } + available, _ := b.Available(1) + r := b.Analyze(available) + var blots int + for _, r := range r { + blots += r.Blots + } + if blots <= 0 { + t.Errorf("expected >0 blots in results, got %d", blots) } } @@ -139,10 +138,10 @@ func BenchmarkAvailable(b *testing.B) { board[SpaceRoll3] = c.roll3 board[SpaceRoll4] = c.roll4 - var available [][]int + var available [][][]int b.ResetTimer() for i := 0; i < b.N; i++ { - available = board.Available(1) + available, _ = board.Available(1) } _ = available @@ -174,12 +173,12 @@ func BenchmarkAnalyze(b *testing.B) { board[SpaceRoll2] = c.roll2 board[SpaceRoll3] = c.roll3 board[SpaceRoll4] = c.roll4 - available := board.Available(1) + available, _ := board.Available(1) var analysis []*Analysis b.ResetTimer() for i := 0; i < b.N; i++ { - analysis = board.Analyze(1, available) + analysis = board.Analyze(available) } _ = analysis diff --git a/cmd/tabula/main.go b/cmd/tabula/main.go index 009880b..fcbb342 100644 --- a/cmd/tabula/main.go +++ b/cmd/tabula/main.go @@ -14,9 +14,9 @@ func main() { b.Print() t := time.Now() - available := b.Available(1) + available, _ := b.Available(1) t2 := time.Now() - analysis := b.Analyze(1, available) + analysis := b.Analyze(available) t3 := time.Now() log.Println("AVAILABLE TOOK ", t2.Sub(t))