diff --git a/event.go b/event.go index 045b6f1..3664a3d 100644 --- a/event.go +++ b/event.go @@ -113,6 +113,7 @@ type EventWin struct { type EventSettings struct { Event + AutoPlay bool Highlight bool Pips bool Moves bool diff --git a/game.go b/game.go index 34fa0c3..8ffd1db 100644 --- a/game.go +++ b/game.go @@ -105,12 +105,12 @@ func (g *Game) Copy() *Game { return newGame } -func (g *Game) NextTurn(replay bool) { +func (g *Game) NextTurn(reroll bool) { if g.Winner != 0 { return } - if !replay { + if !reroll { var nextTurn int8 = 1 if g.Turn == 1 { nextTurn = 2 @@ -410,11 +410,7 @@ ADDMOVES: } } -func (g *Game) LegalMoves(local bool) [][]int8 { - if g.Winner != 0 || g.Roll1 == 0 || g.Roll2 == 0 { - return nil - } - +func (g *Game) DiceRolls() []int8 { rolls := []int8{ g.Roll1, g.Roll2, @@ -425,38 +421,6 @@ func (g *Game) LegalMoves(local bool) [][]int8 { rolls = append(rolls, g.Roll1, g.Roll2) } - haveDiceRoll := func(from, to int8) int8 { - if g.Variant == VariantTabula && to > 12 && to < 25 && ((g.Turn == 1 && !g.Player1.Entered) || (g.Turn == 2 && !g.Player2.Entered)) { - return 0 - } else if (to == SpaceHomePlayer || to == SpaceHomeOpponent) && !g.MayBearOff(g.Turn, false) { - return 0 - } - diff := SpaceDiff(from, to, g.Variant) - if diff == 0 { - return 0 - } - var c int8 - for _, roll := range rolls { - if roll == diff { - c++ - } - } - return c - } - - haveBearOffDiceRoll := func(diff int8) int8 { - if diff == 0 { - return 0 - } - var c int8 - for _, roll := range rolls { - if roll == diff || (roll > diff && g.Variant == VariantBackgammon) { - c++ - } - } - return c - } - useDiceRoll := func(from, to int8) bool { if to == SpaceHomePlayer || to == SpaceHomeOpponent { needRoll := from @@ -494,6 +458,107 @@ func (g *Game) LegalMoves(local bool) [][]int8 { } } + return rolls +} + +func (g *Game) HaveDiceRoll(from int8, to int8) int8 { + if g.Variant == VariantTabula && to > 12 && to < 25 && ((g.Turn == 1 && !g.Player1.Entered) || (g.Turn == 2 && !g.Player2.Entered)) { + return 0 + } else if (to == SpaceHomePlayer || to == SpaceHomeOpponent) && !g.MayBearOff(g.Turn, false) { + return 0 + } + diff := SpaceDiff(from, to, g.Variant) + if diff == 0 { + return 0 + } + var c int8 + for _, roll := range g.DiceRolls() { + if roll == diff { + c++ + } + } + return c +} + +func (g *Game) HaveBearOffDiceRoll(diff int8) int8 { + if diff == 0 { + return 0 + } + var c int8 + for _, roll := range g.DiceRolls() { + if roll == diff || (roll > diff && g.Variant == VariantBackgammon) { + c++ + } + } + 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 { + return nil + } + var moves [][]int8 var movesFound = make(map[int8]bool) @@ -515,7 +580,7 @@ func (g *Game) LegalMoves(local bool) [][]int8 { if false && movesFound[barSpace*100+homeSpace] { return } - available := haveDiceRoll(barSpace, homeSpace) + available := g.HaveDiceRoll(barSpace, homeSpace) if available == 0 { return } @@ -557,10 +622,10 @@ func (g *Game) LegalMoves(local bool) [][]int8 { if false && movesFound[space*100+homeSpace] { continue } - available := haveBearOffDiceRoll(SpaceDiff(space, homeSpace, g.Variant)) + available := g.HaveBearOffDiceRoll(SpaceDiff(space, homeSpace, g.Variant)) if available > 0 { ok := true - if g.Variant == VariantBackgammon && haveDiceRoll(space, homeSpace) == 0 { + 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-- { @@ -595,7 +660,7 @@ func (g *Game) LegalMoves(local bool) [][]int8 { if false && movesFound[space*100+to] { return } - available := haveDiceRoll(space, to) + available := g.HaveDiceRoll(space, to) if available == 0 { return } @@ -620,29 +685,16 @@ func (g *Game) LegalMoves(local bool) [][]int8 { } } - // totalMoves tries all legal moves in a game and returns the maximum total number of moves that a player may consecutively make. - var totalMoves func(in *Game, move []int8) int8 - totalMoves = func(in *Game, move []int8) int8 { - gc := in.Copy() - if !gc.addMove(move) { - log.Panicf("failed to add move %+v to game %+v", move, in) - } - - var maxTotal int8 = 1 - for _, m := range gc.LegalMoves(local) { - total := totalMoves(gc, m) - if total+1 > maxTotal { - maxTotal = total + 1 - } - } - return maxTotal - } - // 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 { - moveCounts[i] = totalMoves(g, move) + 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] } @@ -1100,3 +1152,124 @@ func FormatAndFlipMoves(moves [][]int8, player int8, variant int8) []byte { 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 + } + 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 + } + } + 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 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 + } +} diff --git a/gamestate.go b/gamestate.go index fc6ae4c..5dd2f96 100644 --- a/gamestate.go +++ b/gamestate.go @@ -8,6 +8,7 @@ type GameState struct { *Game PlayerNumber int8 Available [][]int8 // Legal moves. + Forced bool // A forced move is being played automatically. Spectating bool } @@ -118,7 +119,7 @@ func (g *GameState) MayRoll() bool { // MayChooseRoll returns whether the player may send the 'ok' command, supplying // the chosen roll. This command only applies to acey-deucey games. func (g *GameState) MayChooseRoll() bool { - return g.Variant == VariantAceyDeucey && g.Turn != 0 && g.Turn == g.PlayerNumber && ((g.Roll1 == 1 && g.Roll2 == 2) || (g.Roll1 == 2 && g.Roll2 == 1)) + return g.Variant == VariantAceyDeucey && g.Turn != 0 && g.Turn == g.PlayerNumber && ((g.Roll1 == 1 && g.Roll2 == 2) || (g.Roll1 == 2 && g.Roll2 == 1)) && len(g.Moves) == 2 } // MayOK returns whether the player may send the 'ok' command. diff --git a/go.mod b/go.mod index eb0e658..a7612e9 100644 --- a/go.mod +++ b/go.mod @@ -36,7 +36,7 @@ require ( github.com/vanng822/css v1.0.1 // indirect github.com/vanng822/go-premailer v1.20.2 // indirect golang.org/x/crypto v0.18.0 // indirect - golang.org/x/net v0.19.0 // indirect + golang.org/x/net v0.20.0 // indirect golang.org/x/sys v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index 6febfca..7fd7c78 100644 --- a/go.sum +++ b/go.sum @@ -119,8 +119,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= diff --git a/pkg/server/account.go b/pkg/server/account.go index 05cff6d..75c880a 100644 --- a/pkg/server/account.go +++ b/pkg/server/account.go @@ -5,6 +5,7 @@ type account struct { email []byte username []byte password []byte + autoplay bool highlight bool pips bool moves bool diff --git a/pkg/server/client.go b/pkg/server/client.go index 8ad317c..10afc87 100644 --- a/pkg/server/client.go +++ b/pkg/server/client.go @@ -20,6 +20,7 @@ type serverClient struct { active int64 lastPing int64 commands chan []byte + autoplay bool playerNumber int8 terminating bool bgammon.Client diff --git a/pkg/server/database.go b/pkg/server/database.go index ccd52d1..4b244fd 100644 --- a/pkg/server/database.go +++ b/pkg/server/database.go @@ -47,6 +47,7 @@ CREATE TABLE account ( rated_acey_multi integer NOT NULL DEFAULT 150000, rated_tabula_single integer NOT NULL DEFAULT 150000, rated_tabula_multi integer NOT NULL DEFAULT 150000, + autoplay smallint NOT NULL DEFAULT 1, highlight smallint NOT NULL DEFAULT 1, pips smallint NOT NULL DEFAULT 1, moves smallint NOT NULL DEFAULT 0, @@ -341,13 +342,14 @@ func loginAccount(passwordSalt string, username []byte, password []byte) (*accou defer tx.Commit(context.Background()) a := &account{} - var highlight, pips, moves, flip int - err = tx.QueryRow(context.Background(), "SELECT id, email, username, password, highlight, pips, moves, flip FROM account WHERE username = $1 OR email = $2", bytes.ToLower(bytes.TrimSpace(username)), bytes.ToLower(bytes.TrimSpace(username))).Scan(&a.id, &a.email, &a.username, &a.password, &highlight, &pips, &moves, &flip) + var autoplay, highlight, pips, moves, flip int + err = tx.QueryRow(context.Background(), "SELECT id, email, username, password, autoplay, highlight, pips, moves, flip FROM account WHERE username = $1 OR email = $2", bytes.ToLower(bytes.TrimSpace(username)), bytes.ToLower(bytes.TrimSpace(username))).Scan(&a.id, &a.email, &a.username, &a.password, &autoplay, &highlight, &pips, &moves, &flip) if err != nil { return nil, nil } else if len(a.password) == 0 { return nil, fmt.Errorf("account disabled") } + a.autoplay = autoplay == 1 a.highlight = highlight == 1 a.pips = pips == 1 a.moves = moves == 1 diff --git a/pkg/server/game.go b/pkg/server/game.go index dbab0be..4c0ee00 100644 --- a/pkg/server/game.go +++ b/pkg/server/game.go @@ -3,6 +3,8 @@ package server import ( "bufio" "bytes" + "fmt" + "log" "time" "code.rocket9labs.com/tslocum/bgammon" @@ -39,6 +41,82 @@ func newServerGame(id int, variant int8) *serverGame { } } +func (g *serverGame) playForcedMoves() bool { + if g.Winner != 0 || len(g.Moves) != 0 || g.client1 == nil || g.client2 == nil { + return false + } + rolls := g.DiceRolls() + if len(rolls) == 0 { + return false + } + var playerName string + switch g.Turn { + case 1: + if !g.client1.autoplay { + return false + } + playerName = g.Player1.Name + case 2: + if !g.client2.autoplay { + return false + } + playerName = g.Player2.Name + case 0: + return false + } + allMoves := g.TotalMoves(false) + if len(allMoves) == 0 { + return false + } + var forcedMoves [][]int8 + if len(allMoves) == 1 { + forcedMoves = allMoves[0] + } else { + FORCEDMOVES: + for _, m1 := range allMoves[0] { + for _, moves2 := range allMoves[1:] { + var found bool + for _, m2 := range moves2 { + if m1[0] == m2[0] && m1[1] == m2[1] { + found = true + break + } + } + if !found { + continue FORCEDMOVES + } + } + forcedMoves = append(forcedMoves, m1) + } + } + if len(forcedMoves) == 0 { + return false + } + g.eachClient(func(client *serverClient) { + g.sendBoard(client, true) + }) + for _, move := range forcedMoves { + if g.HaveDiceRoll(move[0], move[1]) == 0 { + break + } + ok, _ := g.AddMoves([][]int8{move}, 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), + } + ev.Player = playerName + client.sendEvent(ev) + }) + if g.handleWin() { + return true + } + } + return true +} + func (g *serverGame) roll(player int8) bool { if g.client1 == nil || g.client2 == nil || g.Winner != 0 { return false @@ -81,18 +159,17 @@ func (g *serverGame) roll(player int8) bool { return true } -func (g *serverGame) sendBoard(client *serverClient) { +func (g *serverGame) sendBoard(client *serverClient, forcedMove bool) { if client.json { ev := &bgammon.EventBoard{ GameState: bgammon.GameState{ Game: g.Game, PlayerNumber: client.playerNumber, Available: g.LegalMoves(false), + Forced: forcedMove, + Spectating: g.client1 != client && g.client2 != client, }, } - if g.client1 != client && g.client2 != client { - ev.Spectating = true - } // Reverse spaces for white. if client.playerNumber == 2 { @@ -203,7 +280,7 @@ func (g *serverGame) addClient(client *serverClient) (spectator bool) { } ev.Player = string(client.name) client.sendEvent(ev) - g.sendBoard(client) + g.sendBoard(client, false) return spectator } @@ -215,7 +292,7 @@ func (g *serverGame) addClient(client *serverClient) (spectator bool) { } ev.Player = string(client.name) client.sendEvent(ev) - g.sendBoard(client) + g.sendBoard(client, false) if playerNumber == 0 { return @@ -229,7 +306,7 @@ func (g *serverGame) addClient(client *serverClient) (spectator bool) { } ev.Player = string(client.name) opponent.sendEvent(ev) - g.sendBoard(opponent) + g.sendBoard(opponent, false) } { @@ -240,7 +317,7 @@ func (g *serverGame) addClient(client *serverClient) (spectator bool) { ev.Player = string(client.name) for _, spectator := range g.spectators { spectator.sendEvent(ev) - g.sendBoard(spectator) + g.sendBoard(spectator, false) } } @@ -293,7 +370,7 @@ func (g *serverGame) removeClient(client *serverClient) { client.sendEvent(ev) if !client.json { - g.sendBoard(client) + g.sendBoard(client, false) } var opponent *serverClient @@ -305,14 +382,14 @@ func (g *serverGame) removeClient(client *serverClient) { if opponent != nil { opponent.sendEvent(ev) if !opponent.json { - g.sendBoard(opponent) + g.sendBoard(opponent, false) } } for _, spectator := range g.spectators { spectator.sendEvent(ev) if !spectator.json { - g.sendBoard(spectator) + g.sendBoard(spectator, false) } } @@ -343,7 +420,7 @@ func (g *serverGame) removeClient(client *serverClient) { client.sendEvent(ev) if !client.json { - g.sendBoard(client) + g.sendBoard(client, false) } client.playerNumber = 0 @@ -392,6 +469,196 @@ func (g *serverGame) listing(playerName []byte) *bgammon.GameListing { } } +func (g *serverGame) recordEvent() { + r1, r2, r3 := g.Roll1, g.Roll2, g.Roll3 + if r2 > r1 { + r1, r2 = r2, r1 + } + if r3 > r1 { + r1, r3 = r3, r1 + } + if r3 > r2 { + r2, r3 = r3, r2 + } + var movesFormatted []byte + if len(g.Moves) != 0 { + movesFormatted = append([]byte(" "), bgammon.FormatMoves(g.Moves)...) + } + line := []byte(fmt.Sprintf("%d r %d-%d", g.Turn, r1, r2)) + if r3 > 0 { + line = append(line, []byte(fmt.Sprintf("-%d", r3))...) + } + line = append(line, movesFormatted...) + g.replay = append(g.replay, line) +} + +func (g *serverGame) nextTurn(reroll bool) { + g.Game.NextTurn(reroll) + if reroll { + return + } + + // Roll automatically. + if g.Winner == 0 { + gameState := &bgammon.GameState{ + Game: g.Game, + PlayerNumber: g.Turn, + Available: g.LegalMoves(false), + } + if !gameState.MayDouble() { + if !g.roll(g.Turn) { + g.eachClient(func(client *serverClient) { + client.Terminate("Server error") + }) + return + } + ev := &bgammon.EventRolled{ + Roll1: g.Roll1, + Roll2: g.Roll2, + Roll3: g.Roll3, + } + if g.Turn == 1 { + ev.Player = gameState.Player1.Name + } else { + ev.Player = gameState.Player2.Name + } + g.eachClient(func(client *serverClient) { + client.sendEvent(ev) + }) + + // Play forced moves automatically. + forcedMove := g.playForcedMoves() + if forcedMove && len(g.LegalMoves(false)) == 0 { + chooseRoll := g.Variant == bgammon.VariantAceyDeucey && ((g.Roll1 == 1 && g.Roll2 == 2) || (g.Roll1 == 2 && g.Roll2 == 1)) && len(g.Moves) == 2 + if g.Variant != bgammon.VariantAceyDeucey || !chooseRoll { + g.recordEvent() + g.nextTurn(false) + return + } + } + } + } + + g.eachClient(func(client *serverClient) { + g.sendBoard(client, false) + }) +} + +func (g *serverGame) handleWin() bool { + if g.Winner == 0 { + return false + } + var opponent int8 = 1 + opponentHome := bgammon.SpaceHomePlayer + opponentEntered := g.Player1.Entered + playerBar := bgammon.SpaceBarPlayer + if g.Winner == 1 { + opponent = 2 + opponentHome = bgammon.SpaceHomeOpponent + opponentEntered = g.Player2.Entered + playerBar = bgammon.SpaceBarOpponent + } + + backgammon := bgammon.PlayerCheckers(g.Board[playerBar], opponent) != 0 + if !backgammon { + homeStart, homeEnd := bgammon.HomeRange(g.Winner, g.Variant) + bgammon.IterateSpaces(homeStart, homeEnd, g.Variant, func(space int8, spaceCount int8) { + if bgammon.PlayerCheckers(g.Board[space], opponent) != 0 { + backgammon = true + } + }) + } + + var winPoints int8 + switch g.Variant { + case bgammon.VariantAceyDeucey: + for space := int8(0); space < bgammon.BoardSpaces; space++ { + if (space == bgammon.SpaceHomePlayer || space == bgammon.SpaceHomeOpponent) && opponentEntered { + continue + } + winPoints += bgammon.PlayerCheckers(g.Board[space], opponent) + } + case bgammon.VariantTabula: + winPoints = 1 + default: + if backgammon { + winPoints = 3 // Award backgammon. + } else if g.Board[opponentHome] == 0 { + winPoints = 2 // Award gammon. + } else { + winPoints = 1 + } + } + + g.replay = append([][]byte{[]byte(fmt.Sprintf("i %d %s %s %d %d %d %d %d %d", g.Started.Unix(), g.Player1.Name, g.Player2.Name, g.Points, g.Player1.Points, g.Player2.Points, g.Winner, winPoints, g.Variant))}, g.replay...) + + r1, r2, r3 := g.Roll1, g.Roll2, g.Roll3 + if r2 > r1 { + r1, r2 = r2, r1 + } + if r3 > r1 { + r1, r3 = r3, r1 + } + if r3 > r2 { + r2, r3 = r3, r2 + } + var movesFormatted []byte + if len(g.Moves) != 0 { + movesFormatted = append([]byte(" "), bgammon.FormatMoves(g.Moves)...) + } + line := []byte(fmt.Sprintf("%d r %d-%d", g.Turn, r1, r2)) + if r3 > 0 { + line = append(line, []byte(fmt.Sprintf("-%d", r3))...) + } + line = append(line, movesFormatted...) + g.replay = append(g.replay, line) + + winEvent := &bgammon.EventWin{ + Points: winPoints * g.DoubleValue, + } + var reset bool + if g.Winner == 1 { + winEvent.Player = g.Player1.Name + g.Player1.Points = g.Player1.Points + winPoints*g.DoubleValue + if g.Player1.Points < g.Points { + reset = true + } else { + g.Ended = time.Now() + } + } else { + winEvent.Player = g.Player2.Name + g.Player2.Points = g.Player2.Points + winPoints*g.DoubleValue + if g.Player2.Points < g.Points { + reset = true + } else { + g.Ended = time.Now() + } + } + + winType := winPoints + if g.Variant != bgammon.VariantBackgammon { + winType = 1 + } + err := recordGameResult(g.Game, winType, g.client1.account, g.client2.account, g.replay) + if err != nil { + log.Fatalf("failed to record game result: %s", err) + } + + if !reset { + err := recordMatchResult(g.Game, matchTypeCasual, g.client1.account, g.client2.account) + if err != nil { + log.Fatalf("failed to record match result: %s", err) + } + } else { + g.Reset() + g.replay = g.replay[:0] + } + g.eachClient(func(client *serverClient) { + client.sendEvent(winEvent) + }) + return true +} + func (g *serverGame) terminated() bool { return g.client1 == nil && g.client2 == nil } diff --git a/pkg/server/server.go b/pkg/server/server.go index 5642ba2..f69c7a4 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -196,6 +196,7 @@ func (s *server) handleWebSocket(w http.ResponseWriter, r *http.Request) { connected: now, active: now, commands: commands, + autoplay: true, Client: wsClient, } s.handleClient(c) @@ -301,6 +302,7 @@ func (s *server) handleConnection(conn net.Conn) { connected: now, active: now, commands: commands, + autoplay: true, Client: newSocketClient(conn, commands, events, s.verbose), } s.sendHello(c) diff --git a/pkg/server/server_command.go b/pkg/server/server_command.go index f0a63f3..0dbdfff 100644 --- a/pkg/server/server_command.go +++ b/pkg/server/server_command.go @@ -167,7 +167,9 @@ COMMANDS: cmd.client.account = a.id cmd.client.name = name + cmd.client.autoplay = a.autoplay cmd.client.sendEvent(&bgammon.EventSettings{ + AutoPlay: a.autoplay, Highlight: a.highlight, Pips: a.pips, Moves: a.moves, @@ -520,7 +522,7 @@ COMMANDS: clientGame.eachClient(func(client *serverClient) { if client.json { - clientGame.sendBoard(client) + clientGame.sendBoard(client, false) } }) case bgammon.CommandResign: @@ -603,7 +605,7 @@ COMMANDS: } clientGame.eachClient(func(client *serverClient) { - clientGame.sendBoard(client) + clientGame.sendBoard(client, false) if winEvent != nil { client.sendEvent(winEvent) } @@ -646,7 +648,7 @@ COMMANDS: client.sendEvent(ev) }) - var skipBoard bool + // Re-roll automatically when players roll the same value when starting a game. if clientGame.Turn == 0 && clientGame.Roll1 != 0 && clientGame.Roll2 != 0 { reroll := func() { clientGame.Roll1 = 0 @@ -665,10 +667,8 @@ COMMANDS: ev.Player = string(clientGame.Player2.Name) } clientGame.eachClient(func(client *serverClient) { - clientGame.sendBoard(client) client.sendEvent(ev) }) - skipBoard = true } if clientGame.Roll1 > clientGame.Roll2 { @@ -730,13 +730,22 @@ COMMANDS: } } } - if !skipBoard { - clientGame.eachClient(func(client *serverClient) { - if clientGame.Turn != 0 || !client.json { - clientGame.sendBoard(client) - } - }) + + forcedMove := clientGame.playForcedMoves() + if forcedMove && len(clientGame.LegalMoves(false)) == 0 { + chooseRoll := clientGame.Variant == bgammon.VariantAceyDeucey && ((clientGame.Roll1 == 1 && clientGame.Roll2 == 2) || (clientGame.Roll1 == 2 && clientGame.Roll2 == 1)) && len(clientGame.Moves) == 2 + if clientGame.Variant != bgammon.VariantAceyDeucey || !chooseRoll { + clientGame.recordEvent() + clientGame.nextTurn(false) + continue + } } + + clientGame.eachClient(func(client *serverClient) { + if clientGame.Turn != 0 || !client.json { + clientGame.sendBoard(client, false) + } + }) case bgammon.CommandMove, "m", "mv": if clientGame == nil { cmd.client.sendEvent(&bgammon.EventFailedMove{ @@ -744,7 +753,7 @@ COMMANDS: }) continue } else if clientGame.Winner != 0 { - clientGame.sendBoard(cmd.client) + clientGame.sendBoard(cmd.client, false) continue } @@ -814,115 +823,6 @@ COMMANDS: continue } - var winEvent *bgammon.EventWin - if clientGame.Winner != 0 { - var opponent int8 = 1 - opponentHome := bgammon.SpaceHomePlayer - opponentEntered := clientGame.Player1.Entered - playerBar := bgammon.SpaceBarPlayer - if clientGame.Winner == 1 { - opponent = 2 - opponentHome = bgammon.SpaceHomeOpponent - opponentEntered = clientGame.Player2.Entered - playerBar = bgammon.SpaceBarOpponent - } - - backgammon := bgammon.PlayerCheckers(clientGame.Board[playerBar], opponent) != 0 - if !backgammon { - homeStart, homeEnd := bgammon.HomeRange(clientGame.Winner, clientGame.Variant) - bgammon.IterateSpaces(homeStart, homeEnd, clientGame.Variant, func(space int8, spaceCount int8) { - if bgammon.PlayerCheckers(clientGame.Board[space], opponent) != 0 { - backgammon = true - } - }) - } - - var winPoints int8 - switch clientGame.Variant { - case bgammon.VariantAceyDeucey: - for space := int8(0); space < bgammon.BoardSpaces; space++ { - if (space == bgammon.SpaceHomePlayer || space == bgammon.SpaceHomeOpponent) && opponentEntered { - continue - } - winPoints += bgammon.PlayerCheckers(clientGame.Board[space], opponent) - } - case bgammon.VariantTabula: - winPoints = 1 - default: - if backgammon { - winPoints = 3 // Award backgammon. - } else if clientGame.Board[opponentHome] == 0 { - winPoints = 2 // Award gammon. - } else { - winPoints = 1 - } - } - - clientGame.replay = append([][]byte{[]byte(fmt.Sprintf("i %d %s %s %d %d %d %d %d %d", clientGame.Started.Unix(), clientGame.Player1.Name, clientGame.Player2.Name, clientGame.Points, clientGame.Player1.Points, clientGame.Player2.Points, clientGame.Winner, winPoints, clientGame.Variant))}, clientGame.replay...) - - r1, r2, r3 := clientGame.Roll1, clientGame.Roll2, clientGame.Roll3 - if r2 > r1 { - r1, r2 = r2, r1 - } - if r3 > r1 { - r1, r3 = r3, r1 - } - if r3 > r2 { - r2, r3 = r3, r2 - } - var movesFormatted []byte - if len(clientGame.Moves) != 0 { - movesFormatted = append([]byte(" "), bgammon.FormatMoves(clientGame.Moves)...) - } - line := []byte(fmt.Sprintf("%d r %d-%d", clientGame.Turn, r1, r2)) - if r3 > 0 { - line = append(line, []byte(fmt.Sprintf("-%d", r3))...) - } - line = append(line, movesFormatted...) - clientGame.replay = append(clientGame.replay, line) - - winEvent = &bgammon.EventWin{ - Points: winPoints * clientGame.DoubleValue, - } - var reset bool - if clientGame.Winner == 1 { - winEvent.Player = clientGame.Player1.Name - clientGame.Player1.Points = clientGame.Player1.Points + winPoints*clientGame.DoubleValue - if clientGame.Player1.Points < clientGame.Points { - reset = true - } else { - clientGame.Ended = time.Now() - } - } else { - winEvent.Player = clientGame.Player2.Name - clientGame.Player2.Points = clientGame.Player2.Points + winPoints*clientGame.DoubleValue - if clientGame.Player2.Points < clientGame.Points { - reset = true - } else { - clientGame.Ended = time.Now() - } - } - - winType := winPoints - if clientGame.Variant != bgammon.VariantBackgammon { - winType = 1 - } - err := recordGameResult(clientGame.Game, winType, clientGame.client1.account, clientGame.client2.account, clientGame.replay) - if err != nil { - log.Fatalf("failed to record game result: %s", err) - } - - if !reset { - err := recordMatchResult(clientGame.Game, matchTypeCasual, clientGame.client1.account, clientGame.client2.account) - if err != nil { - log.Fatalf("failed to record match result: %s", err) - } - } else { - clientGame.Reset() - clientGame.replay = clientGame.replay[:0] - } - } - clientGame.eachClient(func(client *serverClient) { ev := &bgammon.EventMoved{ Moves: bgammon.FlipMoves(expandedMoves, client.playerNumber, clientGame.Variant), @@ -930,12 +830,10 @@ COMMANDS: ev.Player = string(cmd.client.name) client.sendEvent(ev) - clientGame.sendBoard(client) - - if winEvent != nil { - client.sendEvent(winEvent) - } + clientGame.sendBoard(client, false) }) + + clientGame.handleWin() case bgammon.CommandReset: if clientGame == nil { cmd.client.sendNotice("You are not currently in a match.") @@ -969,7 +867,7 @@ COMMANDS: ev.Player = string(cmd.client.name) client.sendEvent(ev) - clientGame.sendBoard(client) + clientGame.sendBoard(client, false) }) } case bgammon.CommandOk, "k": @@ -1003,7 +901,7 @@ COMMANDS: clientGame.replay = append(clientGame.replay, []byte(fmt.Sprintf("%d d %d 1", clientGame.Turn, clientGame.DoubleValue))) clientGame.eachClient(func(client *serverClient) { - clientGame.sendBoard(client) + clientGame.sendBoard(client, false) }) } else { cmd.client.sendNotice("Waiting for response from opponent.") @@ -1029,29 +927,6 @@ COMMANDS: continue } - recordEvent := func() { - r1, r2, r3 := clientGame.Roll1, clientGame.Roll2, clientGame.Roll3 - if r2 > r1 { - r1, r2 = r2, r1 - } - if r3 > r1 { - r1, r3 = r3, r1 - } - if r3 > r2 { - r2, r3 = r3, r2 - } - var movesFormatted []byte - if len(clientGame.Moves) != 0 { - movesFormatted = append([]byte(" "), bgammon.FormatMoves(clientGame.Moves)...) - } - line := []byte(fmt.Sprintf("%d r %d-%d", clientGame.Turn, r1, r2)) - if r3 > 0 { - line = append(line, []byte(fmt.Sprintf("-%d", r3))...) - } - line = append(line, movesFormatted...) - clientGame.replay = append(clientGame.replay, line) - } - if clientGame.Variant == bgammon.VariantAceyDeucey && ((clientGame.Roll1 == 1 && clientGame.Roll2 == 2) || (clientGame.Roll1 == 2 && clientGame.Roll2 == 1)) && len(clientGame.Moves) == 2 { var doubles int if len(params) > 0 { @@ -1064,8 +939,8 @@ COMMANDS: continue } - recordEvent() - clientGame.NextTurn(true) + clientGame.recordEvent() + clientGame.nextTurn(true) clientGame.Roll1, clientGame.Roll2 = int8(doubles), int8(doubles) clientGame.Reroll = true @@ -1077,10 +952,11 @@ COMMANDS: } ev.Player = string(cmd.client.name) client.sendEvent(ev) + clientGame.sendBoard(client, false) }) } else if clientGame.Variant == bgammon.VariantAceyDeucey && clientGame.Reroll { - recordEvent() - clientGame.NextTurn(true) + clientGame.recordEvent() + clientGame.nextTurn(true) clientGame.Roll1, clientGame.Roll2 = 0, 0 if !clientGame.roll(cmd.client.playerNumber) { cmd.client.Terminate("Server error") @@ -1096,43 +972,12 @@ COMMANDS: } ev.Player = string(cmd.client.name) client.sendEvent(ev) - clientGame.sendBoard(client) + clientGame.sendBoard(client, false) }) } else { - recordEvent() - clientGame.NextTurn(false) - if clientGame.Winner == 0 { - gameState := &bgammon.GameState{ - Game: clientGame.Game, - PlayerNumber: clientGame.Turn, - Available: clientGame.LegalMoves(false), - } - if !gameState.MayDouble() { - if !clientGame.roll(clientGame.Turn) { - cmd.client.Terminate("Server error") - opponent.Terminate("Server error") - continue - } - clientGame.eachClient(func(client *serverClient) { - ev := &bgammon.EventRolled{ - Roll1: clientGame.Roll1, - Roll2: clientGame.Roll2, - Roll3: clientGame.Roll3, - } - if clientGame.Turn == 1 { - ev.Player = gameState.Player1.Name - } else { - ev.Player = gameState.Player2.Name - } - client.sendEvent(ev) - }) - } - } + clientGame.recordEvent() + clientGame.nextTurn(false) } - - clientGame.eachClient(func(client *serverClient) { - clientGame.sendBoard(client) - }) case bgammon.CommandRematch, "rm": if clientGame == nil { cmd.client.sendNotice("You are not currently in a match.") @@ -1184,7 +1029,7 @@ COMMANDS: ev2.Player = newGame.Player2.Name newGame.client1.sendEvent(ev1) newGame.client1.sendEvent(ev2) - newGame.sendBoard(newGame.client1) + newGame.sendBoard(newGame.client1, false) } { @@ -1200,11 +1045,11 @@ COMMANDS: ev2.Player = newGame.Player1.Name newGame.client2.sendEvent(ev1) newGame.client2.sendEvent(ev2) - newGame.sendBoard(newGame.client2) + newGame.sendBoard(newGame.client2, false) } for _, spectator := range newGame.spectators { - newGame.sendBoard(spectator) + newGame.sendBoard(spectator, false) } } else { clientGame.rematch = cmd.client.playerNumber @@ -1219,7 +1064,7 @@ COMMANDS: continue } - clientGame.sendBoard(cmd.client) + clientGame.sendBoard(cmd.client, false) case bgammon.CommandPassword: if cmd.client.account == 0 { cmd.client.sendNotice("Failed to change password: you are logged in as a guest.") @@ -1242,15 +1087,13 @@ COMMANDS: } cmd.client.sendNotice("Password changed successfully.") case bgammon.CommandSet: - if cmd.client.account == 0 { - continue - } else if len(params) < 2 { + if len(params) < 2 { cmd.client.sendNotice("Please specify the setting name and value as follows: set ") continue } name := string(bytes.ToLower(params[0])) - settings := []string{"highlight", "pips", "moves", "flip"} + settings := []string{"autoplay", "highlight", "pips", "moves", "flip"} var found bool for i := range settings { if name == settings[i] { @@ -1268,6 +1111,14 @@ COMMANDS: cmd.client.sendNotice("Invalid setting value provided.") continue } + + if name == "autoplay" { + cmd.client.autoplay = value == 1 + } + + if cmd.client.account == 0 { + continue + } _ = setAccountSetting(cmd.client.account, name, value) case bgammon.CommandReplay: var ( @@ -1348,7 +1199,7 @@ COMMANDS: log.Println(clientGame.Board[0:28]) clientGame.eachClient(func(client *serverClient) { - clientGame.sendBoard(client) + clientGame.sendBoard(client, false) }) default: log.Printf("Received unknown command from client %s: %s", cmd.client.label(), cmd.command)