Play forced moves automatically

Resolves #5.
This commit is contained in:
Trevor Slocum 2024-01-10 20:03:15 -08:00
parent 118a7f78eb
commit b77a42abd7
11 changed files with 577 additions and 278 deletions

View file

@ -113,6 +113,7 @@ type EventWin struct {
type EventSettings struct {
Event
AutoPlay bool
Highlight bool
Pips bool
Moves bool

297
game.go
View file

@ -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
}
}

View file

@ -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.

2
go.mod
View file

@ -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
)

4
go.sum
View file

@ -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=

View file

@ -5,6 +5,7 @@ type account struct {
email []byte
username []byte
password []byte
autoplay bool
highlight bool
pips bool
moves bool

View file

@ -20,6 +20,7 @@ type serverClient struct {
active int64
lastPing int64
commands chan []byte
autoplay bool
playerNumber int8
terminating bool
bgammon.Client

View file

@ -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

View file

@ -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
}

View file

@ -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)

View file

@ -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 <name> <value>")
continue
}
name := string(bytes.ToLower(params[0]))
settings := []string{"highlight", "pips", "moves", "flip"}