Optimize calculating legal moves

This commit is contained in:
Trevor Slocum 2024-01-10 21:51:49 -08:00
parent b77a42abd7
commit 3eb433e573
4 changed files with 109 additions and 355 deletions

424
game.go
View file

@ -6,6 +6,8 @@ import (
"log"
"strconv"
"time"
"code.rocket9labs.com/tslocum/tabula"
)
var boardTopBlack = []byte("+13-14-15-16-17-18-+---+19-20-21-22-23-24-+")
@ -71,7 +73,7 @@ func NewGame(variant int8) *Game {
return g
}
func (g *Game) Copy() *Game {
func (g *Game) Copy(shallow bool) *Game {
newGame := &Game{
Started: g.Started,
Ended: g.Ended,
@ -94,14 +96,15 @@ func (g *Game) Copy() *Game {
DoubleOffered: g.DoubleOffered,
Reroll: g.Reroll,
boardStates: make([][]int8, len(g.boardStates)),
enteredStates: make([][2]bool, len(g.enteredStates)),
}
copy(newGame.Board, g.Board)
copy(newGame.Moves, g.Moves)
copy(newGame.boardStates, g.boardStates)
copy(newGame.enteredStates, g.enteredStates)
if !shallow {
newGame.boardStates = make([][]int8, len(g.boardStates))
newGame.enteredStates = make([][2]bool, len(g.enteredStates))
copy(newGame.boardStates, g.boardStates)
copy(newGame.enteredStates, g.enteredStates)
}
return newGame
}
@ -276,7 +279,7 @@ func (g *Game) ExpandMove(move []int8, currentSpace int8, moves [][]int8, local
currentSpace = lm[1]
gc := g.Copy()
gc := g.Copy(true)
gc.addMove(lm)
m, ok := gc.ExpandMove(move, currentSpace, newMoves, local)
if ok {
@ -296,7 +299,7 @@ func (g *Game) AddMoves(moves [][]int8, local bool) (bool, [][]int8) {
var addMoves [][]int8
var undoMoves [][]int8
gameCopy := g.Copy()
gameCopy := g.Copy(false)
validateOffset := 0
VALIDATEMOVES:
@ -493,240 +496,35 @@ func (g *Game) HaveBearOffDiceRoll(diff int8) int8 {
return c
}
// totalMoves tries all legal moves in a game and returns all of the possible combinations of moves that a player may make.
func (g *Game) TotalMoves(local bool) [][][]int8 {
var maxMoves int
var allMoves [][][]int8
for _, move := range g.LegalMoves(local) {
for _, newMoves := range g._totalMoves(g.Moves, move, local) {
if len(newMoves) > maxMoves {
maxMoves = len(newMoves)
} else if len(newMoves) < maxMoves {
continue
}
allMoves = append(allMoves, newMoves)
}
}
var newMoves [][][]int8
for _, moves := range allMoves {
if len(moves) == maxMoves {
newMoves = append(newMoves, moves)
}
}
return newMoves
}
// totalMoves tries all legal moves in a game and returns all of the possible combinations of moves that a player may make.
func (g *Game) _totalMoves(moves [][]int8, move []int8, local bool) [][][]int8 {
gc := g.Copy()
if !gc.addMove(move) {
log.Panicf("failed to add move %+v to game %+v", move, g)
}
var allMoves [][][]int8
{
newMoves := append([][]int8{}, moves...)
newMoves = append(newMoves, move)
allMoves = append(allMoves, newMoves)
maxMoves := len(newMoves)
for _, m := range gc.LegalMoves(local) {
for _, newMoves := range gc._totalMoves(newMoves, m, local) {
if len(newMoves) > maxMoves {
maxMoves = len(newMoves)
} else if len(newMoves) < maxMoves {
continue
}
allMoves = append(allMoves, newMoves)
}
}
}
var newMoves [][][]int8
TOTALMOVES:
for _, m1 := range allMoves {
for _, m2 := range newMoves {
if movesEqual(m1, m2) {
continue TOTALMOVES
}
}
newMoves = append(newMoves, m1)
}
return allMoves
}
func (g *Game) LegalMoves(local bool) [][]int8 {
if g.Winner != 0 || g.Roll1 == 0 || g.Roll2 == 0 {
if g.Turn == 0 {
return nil
}
b, ok := g.TabulaBoard()
if !ok {
return nil
}
available, _ := b.Available(g.Turn)
var moves [][]int8
var movesFound = make(map[int8]bool)
var mustEnter bool
var barSpace int8
if PlayerCheckers(g.Board[SpaceBarPlayer], g.Turn) > 0 {
mustEnter = true
barSpace = SpaceBarPlayer
} else if PlayerCheckers(g.Board[SpaceBarOpponent], g.Turn) > 0 {
mustEnter = true
barSpace = SpaceBarOpponent
}
if mustEnter { // Must enter from bar.
from, to := HomeRange(g.opponentPlayer().Number, g.Variant)
if g.Variant == VariantTabula {
from, to = 1, 6
}
IterateSpaces(from, to, g.Variant, func(homeSpace int8, spaceCount int8) {
if false && movesFound[barSpace*100+homeSpace] {
return
for i := range available {
for j := range available[i] {
if available[i][j][0] == 0 && available[i][j][1] == 0 {
break
}
available := g.HaveDiceRoll(barSpace, homeSpace)
if available == 0 {
return
}
opponentCheckers := OpponentCheckers(g.Board[homeSpace], g.Turn)
if opponentCheckers <= 1 {
moves = append(moves, []int8{barSpace, homeSpace})
movesFound[barSpace*100+homeSpace] = true
}
})
} else {
mayBearOff := g.MayBearOff(g.Turn, false)
for sp := range g.Board {
space := int8(sp)
if space == SpaceBarPlayer || space == SpaceBarOpponent { // Handled above.
continue
} else if space == SpaceHomePlayer || space == SpaceHomeOpponent {
homeSpace := SpaceHomePlayer
entered := g.Player1.Entered
if g.Turn == 2 {
homeSpace = SpaceHomeOpponent
entered = g.Player2.Entered
}
if g.Variant == VariantBackgammon || space != homeSpace || entered {
continue
}
}
checkers := g.Board[space]
playerCheckers := PlayerCheckers(checkers, g.Turn)
if playerCheckers == 0 {
continue
}
if mayBearOff {
homeSpace := SpaceHomePlayer
if g.Turn == 2 {
homeSpace = SpaceHomeOpponent
}
if false && movesFound[space*100+homeSpace] {
continue
}
available := g.HaveBearOffDiceRoll(SpaceDiff(space, homeSpace, g.Variant))
if available > 0 {
ok := true
if g.Variant == VariantBackgammon && g.HaveDiceRoll(space, homeSpace) == 0 {
_, homeEnd := HomeRange(g.Turn, g.Variant)
if g.Turn == 2 && g.Variant != VariantTabula {
for homeSpace := space - 1; homeSpace >= homeEnd; homeSpace-- {
if PlayerCheckers(g.Board[homeSpace], g.Turn) != 0 {
ok = false
break
}
}
} else {
for homeSpace := space + 1; homeSpace <= homeEnd; homeSpace++ {
if PlayerCheckers(g.Board[homeSpace], g.Turn) != 0 {
ok = false
break
}
}
}
}
if ok {
moves = append(moves, []int8{space, homeSpace})
movesFound[space*100+homeSpace] = true
if PlayerCheckers(g.Board[available[i][j][0]], g.Turn) != 0 {
var found bool
for _, m := range moves {
if m[0] == available[i][j][0] && m[1] == available[i][j][1] {
found = true
break
}
}
}
// Move normally.
var lastSpace int8 = 0
if g.Turn == 2 || g.Variant == VariantTabula {
lastSpace = 25
}
f := func(to int8, spaceCount int8) {
if false && movesFound[space*100+to] {
return
}
available := g.HaveDiceRoll(space, to)
if available == 0 {
return
}
opponentCheckers := OpponentCheckers(g.Board[to], g.Turn)
if opponentCheckers <= 1 {
moves = append(moves, []int8{space, to})
movesFound[space*100+to] = true
if !found {
moves = append(moves, []int8{available[i][j][0], available[i][j][1]})
}
}
if space == SpaceHomePlayer {
iterateSpace := int8(25)
if g.Variant == VariantTabula {
iterateSpace = 1
}
IterateSpaces(iterateSpace, lastSpace, g.Variant, f)
} else if space == SpaceHomeOpponent {
IterateSpaces(1, lastSpace, g.Variant, f)
} else {
IterateSpaces(space, lastSpace, g.Variant, f)
}
}
}
// Simulate all possible moves to their final value and only allow moves that will achieve the maximum total moves.
var maxMoves int8
moveCounts := make([]int8, len(moves))
for i, move := range moves {
var moveCount int
allMoves := g._totalMoves(g.Moves, move, local)
if len(allMoves) > 0 {
moveCount = len(allMoves[0])
}
moveCounts[i] = int8(moveCount)
if moveCounts[i] > maxMoves {
maxMoves = moveCounts[i]
}
}
if maxMoves > 1 {
var newMoves [][]int8
for i, move := range moves {
if moveCounts[i] >= maxMoves {
newMoves = append(newMoves, move)
}
}
moves = newMoves
}
replaceSpace := func(i int8) int8 {
if g.Turn == 1 && i == SpaceHomeOpponent {
return SpaceHomePlayer
} else if g.Turn == 1 && i == SpaceBarOpponent {
return SpaceBarPlayer
} else if g.Turn == 2 && i == SpaceHomePlayer {
return SpaceHomeOpponent
} else if g.Turn == 2 && i == SpaceBarPlayer {
return SpaceBarOpponent
}
return i
}
for i := range moves {
for j := range moves[i] {
moves[i][j] = replaceSpace(moves[i][j])
}
}
return moves
}
@ -1153,123 +951,57 @@ func ValidSpace(space int8) bool {
return space >= 0 && space <= 27
}
func movesEqual(a [][]int8, b [][]int8) bool {
l := len(a)
if len(b) != l {
return false
func (g *Game) TabulaBoard() (tabula.Board, bool) {
var roll1, roll2, roll3, roll4 int8
roll1, roll2 = int8(g.Roll1), int8(g.Roll2)
if g.Variant == VariantTabula {
roll3 = int8(g.Roll3)
} else if roll1 == roll2 {
roll3, roll4 = int8(g.Roll1), int8(g.Roll2)
}
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:
if a[0][0] == b[0][0] && a[0][1] == b[0][1] { // 1
if (a[1][0] == b[1][0] && a[1][1] == b[1][1] && a[2][0] == b[2][0] && a[2][1] == b[2][1]) || // 2, 3
(a[1][0] == b[2][0] && a[1][1] == b[2][1] && a[2][0] == b[1][0] && a[2][1] == b[1][1]) { // 3, 2
return true
}
entered1, entered2 := int8(1), int8(1)
if g.Variant != VariantBackgammon {
if !g.Player1.Entered {
entered1 = 0
}
if a[0][0] == b[1][0] && a[0][1] == b[1][1] { // 2
if (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[1][0] == b[2][0] && a[1][1] == b[2][1] && a[2][0] == b[0][0] && a[2][1] == b[0][1]) {
return true
}
if !g.Player2.Entered {
entered2 = 0
}
if a[0][0] == b[2][0] && a[0][1] == b[2][1] { // 3
if (a[1][0] == b[0][0] && a[1][1] == b[0][1] && a[2][0] == b[1][0] && a[2][1] == b[1][1]) || // 1, 2
(a[1][0] == b[1][0] && a[1][1] == b[1][1] && a[2][0] == b[0][0] && a[2][1] == b[0][1]) { // 2, 1
return true
}
}
return false
case 4:
if a[0][0] == b[0][0] && a[0][1] == b[0][1] { // 1
if a[1][0] == b[1][0] && a[1][1] == b[1][1] { // 2
if (a[2][0] == b[2][0] && a[2][1] == b[2][1] && a[3][0] == b[3][0] && a[3][1] == b[3][1]) || // 3,4
(a[2][0] == b[3][0] && a[2][1] == b[3][1] && a[3][0] == b[2][0] && a[3][1] == b[2][1]) { // 4,3
return true
}
}
if a[1][0] == b[2][0] && a[1][1] == b[2][1] { // 3
if (a[2][0] == b[1][0] && a[2][1] == b[1][1] && a[3][0] == b[3][0] && a[3][1] == b[3][1]) || // 2,4
(a[2][0] == b[3][0] && a[2][1] == b[3][1] && a[3][0] == b[1][0] && a[3][1] == b[1][1]) { // 4,2
return true
}
}
if a[1][0] == b[3][0] && a[1][1] == b[3][1] { // 4
if (a[2][0] == b[2][0] && a[2][1] == b[2][1] && a[3][0] == b[1][0] && a[3][1] == b[1][1]) || // 3,2
(a[2][0] == b[1][0] && a[2][1] == b[1][1] && a[3][0] == b[2][0] && a[3][1] == b[2][1]) { // 2,3
return true
}
}
}
if a[0][0] == b[1][0] && a[0][1] == b[1][1] { // 2
if a[1][0] == b[0][0] && a[1][1] == b[0][1] { // 1
if (a[2][0] == b[2][0] && a[2][1] == b[2][1] && a[3][0] == b[3][0] && a[3][1] == b[3][1]) || // 3,4
(a[2][0] == b[3][0] && a[2][1] == b[3][1] && a[3][0] == b[2][0] && a[3][1] == b[2][1]) { // 4,3
return true
}
}
if a[1][0] == b[2][0] && a[1][1] == b[2][1] { // 3
if (a[2][0] == b[3][0] && a[2][1] == b[3][1] && a[3][0] == b[0][0] && a[3][1] == b[0][1]) || // 4,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]) { // 1,4
return true
}
}
if a[1][0] == b[3][0] && a[1][1] == b[3][1] { // 4
if (a[2][0] == b[2][0] && a[2][1] == b[2][1] && a[3][0] == b[0][0] && a[3][1] == b[0][1]) || // 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]) { // 1,3
return true
}
}
}
if a[0][0] == b[2][0] && a[0][1] == b[2][1] { // 3
if a[1][0] == b[0][0] && a[1][1] == b[0][1] { // 1
if (a[2][0] == b[1][0] && a[2][1] == b[1][1] && a[3][0] == b[3][0] && a[3][1] == b[3][1]) || // 2,4
(a[2][0] == b[3][0] && a[2][1] == b[3][1] && a[3][0] == b[1][0] && a[3][1] == b[1][1]) { // 4,2
return true
}
}
if a[1][0] == b[1][0] && a[1][1] == b[1][1] { // 2
if (a[2][0] == b[0][0] && a[2][1] == b[0][1] && a[3][0] == b[3][0] && a[3][1] == b[3][1]) || // 1,4
(a[2][0] == b[3][0] && a[2][1] == b[3][1] && a[3][0] == b[0][0] && a[3][1] == b[0][1]) { // 4,1
return true
}
}
if a[1][0] == b[3][0] && a[1][1] == b[3][1] { // 4
if (a[2][0] == b[1][0] && a[2][1] == b[1][1] && a[3][0] == b[0][0] && a[3][1] == b[0][1]) || // 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]) { // 1,2
return true
}
}
}
if a[0][0] == b[3][0] && a[0][1] == b[3][1] { // 4
if a[1][0] == b[0][0] && a[1][1] == b[0][1] { // 1
if (a[2][0] == b[2][0] && a[2][1] == b[2][1] && a[3][0] == b[1][0] && a[3][1] == b[1][1]) || // 3,2
(a[2][0] == b[1][0] && a[2][1] == b[1][1] && a[3][0] == b[2][0] && a[3][1] == b[2][1]) { // 2,3
return true
}
}
if a[1][0] == b[1][0] && a[1][1] == b[1][1] { // 2
if (a[2][0] == b[0][0] && a[2][1] == b[0][1] && a[3][0] == b[2][0] && a[3][1] == b[2][1]) || // 1,3
(a[2][0] == b[2][0] && a[2][1] == b[2][1] && a[3][0] == b[0][0] && a[3][1] == b[0][1]) { // 3,1
return true
}
}
if a[1][0] == b[2][0] && a[1][1] == b[2][1] { // 3
if (a[2][0] == b[0][0] && a[2][1] == b[0][1] && a[3][0] == b[1][0] && a[3][1] == b[1][1]) || // 1,2
(a[2][0] == b[1][0] && a[2][1] == b[1][1] && a[3][0] == b[0][0] && a[3][1] == b[0][1]) { // 2,1
return true
}
}
}
return false
default:
log.Panicf("more than 4 moves were provided: %+v %+v", a, b)
return false
}
b := g.Board
tb := tabula.Board{b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15], b[16], b[17], b[18], b[19], b[20], b[21], b[22], b[23], b[24], b[25], b[26], b[27], roll1, roll2, roll3, roll4, entered1, entered2, g.Variant}
for _, move := range g.Moves {
diff := SpaceDiff(move[0], move[1], g.Variant)
if diff == 0 {
return tabula.Board{}, false
}
if tb[tabula.SpaceRoll1] == diff {
tb[tabula.SpaceRoll1] = 0
continue
} else if tb[tabula.SpaceRoll2] == diff {
tb[tabula.SpaceRoll2] = 0
continue
} else if tb[tabula.SpaceRoll3] == diff {
tb[tabula.SpaceRoll3] = 0
continue
} else if tb[tabula.SpaceRoll4] == diff {
tb[tabula.SpaceRoll4] = 0
continue
}
var highest = tabula.SpaceRoll1
if tb[tabula.SpaceRoll2] > tb[tabula.SpaceRoll1] {
highest = tabula.SpaceRoll2
}
if tb[tabula.SpaceRoll3] > tb[highest] {
highest = tabula.SpaceRoll3
}
if tb[tabula.SpaceRoll4] > tb[highest] {
highest = tabula.SpaceRoll4
}
if tb[highest] < diff {
return tabula.Board{}, false
}
tb[highest] = 0
}
return tb, true
}

2
go.mod
View file

@ -3,6 +3,7 @@ module code.rocket9labs.com/tslocum/bgammon
go 1.17
require (
code.rocket9labs.com/tslocum/tabula v0.0.0-20240108183445-695ea428ae21
github.com/alexedwards/argon2id v1.0.0
github.com/gobwas/ws v1.3.2
github.com/gorilla/mux v1.8.1
@ -12,6 +13,7 @@ require (
)
require (
code.rocket9labs.com/tslocum/bei v0.0.0-20240108012722-6db380cc190b // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible // indirect

4
go.sum
View file

@ -1,3 +1,7 @@
code.rocket9labs.com/tslocum/bei v0.0.0-20240108012722-6db380cc190b h1:Y0a14Kf/hSYepSmp4ZfDeE4CZZGBGBS97CNjCbKJm0c=
code.rocket9labs.com/tslocum/bei v0.0.0-20240108012722-6db380cc190b/go.mod h1:tS60/VNAJphKvDBkSLQhKALa15msIAuWWfEKNc4oFZc=
code.rocket9labs.com/tslocum/tabula v0.0.0-20240108183445-695ea428ae21 h1:1VG88tdhCSVv7wGoIKQe8A8KfBXJsdz5pDsyP4ymDwk=
code.rocket9labs.com/tslocum/tabula v0.0.0-20240108183445-695ea428ae21/go.mod h1:WEJXESKXqrMFLAArikQ79lpRibNeeE1C0VruxXYMF5M=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=

View file

@ -64,20 +64,36 @@ func (g *serverGame) playForcedMoves() bool {
case 0:
return false
}
allMoves := g.TotalMoves(false)
tb, ok := g.TabulaBoard()
if !ok {
return false
}
allMoves, _ := tb.Available(g.Turn)
if len(allMoves) == 0 {
return false
}
var forcedMoves [][]int8
var forcedMoves [][2]int8
if len(allMoves) == 1 {
forcedMoves = allMoves[0]
for i := range allMoves {
for j := 0; j < 4; j++ {
if allMoves[i][j][0] == 0 && allMoves[i][j][1] == 0 {
break
}
forcedMoves = append(forcedMoves, allMoves[i][j])
}
}
} else {
FORCEDMOVES:
for _, m1 := range allMoves[0] {
for _, moves2 := range allMoves[1:] {
for i := range allMoves {
if i == 0 {
continue
}
var found bool
for _, m2 := range moves2 {
if m1[0] == m2[0] && m1[1] == m2[1] {
for j := 0; j < 4; j++ {
if allMoves[i][j][0] == 0 && allMoves[i][j][1] == 0 {
break
} else if allMoves[i][j][0] == m1[0] && allMoves[i][j][1] == m1[1] {
found = true
break
}
@ -99,13 +115,13 @@ func (g *serverGame) playForcedMoves() bool {
if g.HaveDiceRoll(move[0], move[1]) == 0 {
break
}
ok, _ := g.AddMoves([][]int8{move}, false)
ok, _ := g.AddMoves([][]int8{{move[0], move[1]}}, false)
if !ok {
log.Fatalf("failed to play forced move %v: %v %v (%v) (%v)", move, forcedMoves, g.DiceRolls(), g.Game, g.Board)
}
g.eachClient(func(client *serverClient) {
ev := &bgammon.EventMoved{
Moves: bgammon.FlipMoves([][]int8{move}, client.playerNumber, g.Variant),
Moves: bgammon.FlipMoves([][]int8{{move[0], move[1]}}, client.playerNumber, g.Variant),
}
ev.Player = playerName
client.sendEvent(ev)
@ -173,7 +189,7 @@ func (g *serverGame) sendBoard(client *serverClient, forcedMove bool) {
// Reverse spaces for white.
if client.playerNumber == 2 {
ev.GameState.Game = ev.GameState.Copy()
ev.GameState.Game = ev.GameState.Copy(true)
ev.GameState.PlayerNumber = 1
ev.GameState.Player1, ev.GameState.Player2 = ev.GameState.Player2, ev.GameState.Player1