bgammon/pkg/server/game.go

736 lines
17 KiB
Go

package server
import (
"bufio"
"bytes"
"fmt"
"log"
"time"
"code.rocket9labs.com/tslocum/bgammon"
)
type serverGame struct {
id int
created int64
active int64
name []byte
password []byte
client1 *serverClient
client2 *serverClient
spectators []*serverClient
allowed1 []byte
allowed2 []byte
account1 int
account2 int
inactive int8
forefeit int8
rematch int8
rejoin1 bool
rejoin2 bool
replay [][]byte
*bgammon.Game
}
func newServerGame(id int, variant int8) *serverGame {
now := time.Now().Unix()
return &serverGame{
id: id,
created: now,
active: now,
Game: bgammon.NewGame(variant),
}
}
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
}
tb, ok := g.TabulaBoard()
if !ok {
return false
}
allMoves, allBoards := tb.Available(g.Turn)
if len(allMoves) == 0 {
return false
}
for i := range allBoards {
if i == 0 {
continue
} else if allBoards[i] != allBoards[0] {
return false
}
}
var forcedMoves [][2]int8
for _, m1 := range allMoves[0] {
if m1[0] == 0 && m1[1] == 0 {
break
}
forcedMoves = append(forcedMoves, m1)
}
if len(forcedMoves) == 0 {
return false
}
gc := g.Copy(true)
for _, move := range forcedMoves {
if gc.Winner != 0 {
break
} else if gc.HaveDiceRoll(move[0], move[1]) == 0 {
return false
}
ok, _ := gc.AddMoves([][]int8{{move[0], move[1]}}, false)
if !ok {
log.Printf("ERROR: failed to play forced move during validation %v: %v %v (%v) (%v) (%v)", move, forcedMoves, gc.DiceRolls(), gc, gc.Board, allMoves)
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[0], move[1]}}, false)
if !ok {
log.Printf("ERROR: failed to play forced move %v: %v %v (%v) (%v) (%v)", move, forcedMoves, g.DiceRolls(), g.Game, g.Board, allMoves)
g.eachClient(func(client *serverClient) {
g.sendBoard(client, false)
})
return false
}
g.eachClient(func(client *serverClient) {
ev := &bgammon.EventMoved{
Moves: bgammon.FlipMoves([][]int8{{move[0], move[1]}}, client.playerNumber, g.Variant),
}
ev.Player = playerName
client.sendEvent(ev)
})
if g.handleWin() {
return true
}
}
g.NextPartialTurn(g.Turn)
return true
}
func (g *serverGame) roll(player int8) bool {
if g.client1 == nil || g.client2 == nil || g.Winner != 0 {
return false
}
if g.Turn == 0 {
if player == 1 {
if g.Roll1 != 0 {
return false
}
g.Roll1 = int8(RandInt(6) + 1)
} else {
if g.Roll2 != 0 {
return false
}
g.Roll2 = int8(RandInt(6) + 1)
}
// Only allow the same players to rejoin the game.
if g.allowed1 == nil {
g.allowed1, g.allowed2 = g.client1.name, g.client2.name
}
// Store account IDs.
if g.Started == 0 && g.Roll1 != 0 && g.Roll2 != 0 {
g.Started = time.Now().Unix()
if g.client1.account != nil {
g.account1 = g.client1.account.id
}
if g.client2.account != nil {
g.account2 = g.client2.account.id
}
}
return true
} else if player != g.Turn || g.Roll1 != 0 || g.Roll2 != 0 {
return false
}
g.Roll1 = int8(RandInt(6) + 1)
g.Roll2 = int8(RandInt(6) + 1)
if g.Variant == bgammon.VariantTabula {
g.Roll3 = int8(RandInt(6) + 1)
}
return true
}
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,
},
}
// Reverse spaces for white.
if client.playerNumber == 2 {
ev.GameState.Game = ev.GameState.Copy(true)
ev.GameState.PlayerNumber = 1
ev.GameState.Player1, ev.GameState.Player2 = ev.GameState.Player2, ev.GameState.Player1
ev.GameState.Player1.Number = 1
ev.GameState.Player2.Number = 2
switch ev.GameState.Turn {
case 1:
ev.GameState.Turn = 2
case 2:
ev.GameState.Turn = 1
}
switch ev.GameState.DoublePlayer {
case 1:
ev.GameState.DoublePlayer = 2
case 2:
ev.GameState.DoublePlayer = 1
}
switch ev.GameState.Winner {
case 1:
ev.GameState.Winner = 2
case 2:
ev.GameState.Winner = 1
}
if ev.GameState.Roll1 == 0 || ev.GameState.Roll2 == 0 {
ev.GameState.Roll1, ev.GameState.Roll2 = ev.GameState.Roll2, ev.GameState.Roll1
}
// Flip board.
if g.Variant == bgammon.VariantTabula {
for space := int8(1); space <= 24; space++ {
ev.Board[space] = g.Game.Board[space] * -1
}
} else {
for space := int8(1); space <= 24; space++ {
ev.Board[space] = g.Game.Board[bgammon.FlipSpace(space, client.playerNumber, g.Variant)] * -1
}
}
ev.Board[bgammon.SpaceHomePlayer], ev.Board[bgammon.SpaceHomeOpponent] = ev.Board[bgammon.SpaceHomeOpponent]*-1, ev.Board[bgammon.SpaceHomePlayer]*-1
ev.Board[bgammon.SpaceBarPlayer], ev.Board[bgammon.SpaceBarOpponent] = ev.Board[bgammon.SpaceBarOpponent]*-1, ev.Board[bgammon.SpaceBarPlayer]*-1
ev.Moves = bgammon.FlipMoves(g.Game.Moves, client.playerNumber, g.Variant)
ev.GameState.Available = g.LegalMoves(false)
for i := range ev.GameState.Available {
ev.GameState.Available[i][0], ev.GameState.Available[i][1] = bgammon.FlipSpace(ev.GameState.Available[i][0], client.playerNumber, g.Variant), bgammon.FlipSpace(ev.GameState.Available[i][1], client.playerNumber, g.Variant)
}
}
// Sort available moves.
bgammon.SortMoves(ev.Available)
client.sendEvent(ev)
return
}
scanner := bufio.NewScanner(bytes.NewReader(g.BoardState(client.playerNumber, false)))
for scanner.Scan() {
client.sendNotice(string(scanner.Bytes()))
}
}
func (g *serverGame) playerCount() int8 {
var c int8
if g.client1 != nil {
c++
}
if g.client2 != nil {
c++
}
return c
}
func (g *serverGame) eachClient(f func(client *serverClient)) {
if g.client1 != nil {
f(g.client1)
}
if g.client2 != nil {
f(g.client2)
}
for _, spectator := range g.spectators {
f(spectator)
}
}
func (g *serverGame) addClient(client *serverClient) (spectator bool) {
if g.allowed1 != nil && !bytes.Equal(client.name, g.allowed1) && !bytes.Equal(client.name, g.allowed2) {
spectator = true
} else if g.client1 != nil && g.client2 != nil {
spectator = true
}
if spectator {
for _, spec := range g.spectators {
if spec == client {
return true
}
}
client.playerNumber = 1
g.spectators = append(g.spectators, client)
ev := &bgammon.EventJoined{
GameID: g.id,
PlayerNumber: 1,
}
ev.Player = string(client.name)
client.sendEvent(ev)
g.sendBoard(client, false)
return spectator
}
var playerNumber int8
defer func() {
ev := &bgammon.EventJoined{
GameID: g.id,
PlayerNumber: 1,
}
ev.Player = string(client.name)
client.sendEvent(ev)
g.sendBoard(client, false)
if playerNumber == 0 {
return
}
opponent := g.opponent(client)
if opponent != nil {
ev := &bgammon.EventJoined{
GameID: g.id,
PlayerNumber: 2,
}
ev.Player = string(client.name)
opponent.sendEvent(ev)
g.sendBoard(opponent, false)
}
{
ev := &bgammon.EventJoined{
GameID: g.id,
PlayerNumber: client.playerNumber,
}
ev.Player = string(client.name)
for _, spectator := range g.spectators {
spectator.sendEvent(ev)
g.sendBoard(spectator, false)
}
}
if playerNumber == 1 {
g.rejoin1 = true
} else {
g.rejoin2 = true
}
if g.forefeit == playerNumber {
g.forefeit = 0
}
}()
var rating int
var icon int
if client.account != nil {
rating = client.account.casual.getRating(g.Variant, g.Points > 1) / 100
icon = client.account.icon
}
switch {
case g.client1 != nil:
g.client2 = client
g.Player2.Name = string(client.name)
g.Player2.Rating = rating
g.Player2.Icon = icon
client.playerNumber = 2
playerNumber = 2
case g.client2 != nil:
g.client1 = client
g.Player1.Name = string(client.name)
g.Player1.Rating = rating
g.Player1.Icon = icon
client.playerNumber = 1
playerNumber = 1
default:
if RandInt(2) == 0 {
g.client1 = client
g.Player1.Name = string(client.name)
g.Player1.Rating = rating
g.Player1.Icon = icon
client.playerNumber = 1
playerNumber = 1
} else {
g.client2 = client
g.Player2.Name = string(client.name)
g.Player2.Rating = rating
g.Player2.Icon = icon
client.playerNumber = 2
playerNumber = 2
}
}
return spectator
}
func (g *serverGame) removeClient(client *serverClient) {
var playerNumber int
defer func() {
if playerNumber == 0 {
return
}
ev := &bgammon.EventLeft{}
ev.Player = string(client.name)
client.sendEvent(ev)
if !client.json {
g.sendBoard(client, false)
}
var opponent *serverClient
if playerNumber == 1 && g.client2 != nil {
opponent = g.client2
} else if playerNumber == 2 && g.client1 != nil {
opponent = g.client1
}
if opponent != nil {
opponent.sendEvent(ev)
if !opponent.json {
g.sendBoard(opponent, false)
}
}
for _, spectator := range g.spectators {
spectator.sendEvent(ev)
if !spectator.json {
g.sendBoard(spectator, false)
}
}
if playerNumber == 1 && g.client2 != nil {
g.forefeit = 1
} else if playerNumber == 2 && g.client1 != nil {
g.forefeit = 2
}
client.playerNumber = 0
}()
switch {
case g.client1 == client:
g.client1 = nil
g.Player1.Name = ""
g.Player1.Rating = 0
g.Player1.Icon = 0
playerNumber = 1
case g.client2 == client:
g.client2 = nil
g.Player2.Name = ""
g.Player2.Rating = 0
g.Player2.Icon = 0
playerNumber = 2
default:
for i, spectator := range g.spectators {
if spectator == client {
g.spectators = append(g.spectators[:i], g.spectators[i+1:]...)
ev := &bgammon.EventLeft{}
ev.Player = string(client.name)
client.sendEvent(ev)
if !client.json {
g.sendBoard(client, false)
}
client.playerNumber = 0
return
}
}
return
}
}
func (g *serverGame) opponent(client *serverClient) *serverClient {
if g.client1 == client {
return g.client2
} else if g.client2 == client {
return g.client1
}
return nil
}
func (g *serverGame) listing(playerName []byte) *bgammon.GameListing {
if g.terminated() {
return nil
}
var playerCount int8
if len(g.allowed1) != 0 && (len(playerName) == 0 || (!bytes.Equal(g.allowed1, playerName) && !bytes.Equal(g.allowed2, playerName))) {
playerCount = 2
} else {
playerCount = g.playerCount()
}
var rating int
if g.client1 != nil && g.client1.account != nil {
rating = g.client1.account.casual.getRating(g.Variant, g.Points > 1)
}
if g.client2 != nil && g.client2.account != nil {
r := g.client2.account.casual.getRating(g.Variant, g.Points > 1)
if r > rating {
rating = r
}
}
name := string(g.name)
switch g.Variant {
case bgammon.VariantAceyDeucey:
name = "(Acey-deucey) " + name
case bgammon.VariantTabula:
name = "(Tabula) " + name
}
return &bgammon.GameListing{
ID: g.id,
Points: g.Points,
Password: len(g.password) != 0,
Players: playerCount,
Rating: rating / 100,
Name: name,
}
}
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) addReplayHeader() {
g.replay = append([][]byte{[]byte(fmt.Sprintf("i %d %s %s %d %d %d %d %d %d", g.Started, g.allowed1, g.allowed2, g.Points, g.Player1.Points, g.Player2.Points, g.Winner, g.DoubleValue, g.Variant))}, g.replay...)
}
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
}
// Check for backgammon win.
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
}
})
}
// Calculate win type and point value.
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
}
}
// Finalize replay.
g.addReplayHeader()
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)
// Create win event.
winEvent := &bgammon.EventWin{
Points: winPoints * g.DoubleValue,
}
var reset bool
if g.Winner == 1 {
winEvent.Player = g.Player1.Name
g.Player1.Points = add8(g.Player1.Points, mul8(winPoints, g.DoubleValue))
if g.Player1.Points < g.Points {
reset = true
} else {
g.Ended = time.Now().Unix()
}
} else {
winEvent.Player = g.Player2.Name
g.Player2.Points = add8(g.Player2.Points, mul8(winPoints, g.DoubleValue))
if g.Player2.Points < g.Points {
reset = true
} else {
g.Ended = time.Now().Unix()
}
}
// Record game.
winType := winPoints
if g.Variant != bgammon.VariantBackgammon {
winType = 1
}
err := recordGameResult(g, winType, g.replay)
if err != nil {
log.Fatalf("failed to record game result: %s", err)
}
if !reset {
// Record match.
err := recordMatchResult(g, matchTypeCasual)
if err != nil {
log.Fatalf("failed to record match result: %s", err)
}
} else {
// Reset game and continue match.
g.Reset()
g.replay = g.replay[:0]
}
// Refresh cached ratings.
if g.client1 != nil && g.client1.account != nil {
g.Player1.Rating = g.client1.account.casual.getRating(g.Variant, g.Points > 1) / 100
}
if g.client2 != nil && g.client2.account != nil {
g.Player2.Rating = g.client2.account.casual.getRating(g.Variant, g.Points > 1) / 100
}
// Send board and win events.
g.eachClient(func(client *serverClient) {
g.sendBoard(client, false)
client.sendEvent(winEvent)
})
return true
}
func (g *serverGame) terminated() bool {
return g.client1 == nil && g.client2 == nil
}