Allow forfeiting match after more than ten minutes of inactivity

Resolves #15.
This commit is contained in:
Trevor Slocum 2024-01-17 13:03:20 -08:00
parent 89f20dffb9
commit 3607efee41
5 changed files with 123 additions and 11 deletions

59
game.go
View file

@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"log"
"math"
"strconv"
"time"
@ -47,6 +48,10 @@ type Game struct {
Reroll bool // Used in acey-deucey.
partialTurn int8
partialTime time.Time
partialHandled bool
boardStates [][]int8 // One board state for each move to allow undoing a move.
enteredStates [][2]bool // Player 1 entered state and Player 2 entered state for each move.
@ -96,6 +101,10 @@ func (g *Game) Copy(shallow bool) *Game {
DoubleOffered: g.DoubleOffered,
Reroll: g.Reroll,
partialTurn: g.partialTurn,
partialTime: g.partialTime,
partialHandled: g.partialHandled,
}
copy(newGame.Board, g.Board)
copy(newGame.Moves, g.Moves)
@ -108,6 +117,50 @@ func (g *Game) Copy(shallow bool) *Game {
return newGame
}
func (g *Game) PartialTurn() int8 {
return g.partialTurn
}
func (g *Game) PartialTime() int {
var delta time.Duration
if g.partialTime.IsZero() {
delta = time.Since(g.Started)
} else {
delta = time.Since(g.partialTime)
}
if delta <= 30*time.Second {
return 0
}
return int(math.Floor(delta.Seconds()))
}
func (g *Game) PartialHandled() bool {
return g.partialHandled
}
func (g *Game) SetPartialHandled(handled bool) {
g.partialHandled = handled
}
func (g *Game) NextPartialTurn(player int8) {
if g.Started.IsZero() || g.Winner != 0 {
return
}
delta := g.PartialTime()
if delta > 0 {
switch g.partialTurn {
case 1:
g.Player1.Inactive += delta
case 2:
g.Player2.Inactive += delta
}
}
g.partialTurn = player
g.partialTime = time.Now()
}
func (g *Game) NextTurn(reroll bool) {
if g.Winner != 0 {
return
@ -121,6 +174,8 @@ func (g *Game) NextTurn(reroll bool) {
g.Turn = nextTurn
}
g.NextPartialTurn(g.Turn)
g.Roll1, g.Roll2, g.Roll3 = 0, 0, 0
g.Moves = g.Moves[:0]
g.boardStates = g.boardStates[:0]
@ -128,6 +183,8 @@ func (g *Game) NextTurn(reroll bool) {
}
func (g *Game) Reset() {
g.Player1.Inactive = 0
g.Player2.Inactive = 0
if g.Variant != VariantBackgammon {
g.Player1.Entered = false
g.Player2.Entered = false
@ -144,6 +201,8 @@ func (g *Game) Reset() {
g.Reroll = false
g.boardStates = nil
g.enteredStates = nil
g.partialTurn = 0
g.partialTime = time.Time{}
}
func (g *Game) turnPlayer() Player {

View file

@ -23,6 +23,7 @@ type serverGame struct {
allowed2 []byte
account1 int
account2 int
inactive int8
forefeit int8
rematch int8
rejoin1 bool
@ -130,6 +131,7 @@ func (g *serverGame) playForcedMoves() bool {
return true
}
}
g.NextPartialTurn(g.Turn)
return true
}

View file

@ -23,6 +23,8 @@ import (
const clientTimeout = 40 * time.Second
const inactiveLimit = 600 // 10 minutes.
var allowDebugCommands bool
var (
@ -134,7 +136,7 @@ func NewServer(tz string, dataSource string, mailServer string, passwordSalt str
go s.handleNewGameIDs()
go s.handleNewClientIDs()
go s.handleCommands()
go s.handleTerminatedGames()
go s.handleGames()
return s
}
@ -242,20 +244,62 @@ func (s *server) removeClient(c *serverClient) {
}
}
func (s *server) handleTerminatedGames() {
func (s *server) handleGames() {
t := time.NewTicker(time.Minute)
for range t.C {
s.gamesLock.Lock()
i := 0
for _, g := range s.games {
if !g.PartialHandled() && g.Player1.Rating != 0 && g.Player2.Rating != 0 {
partialTurn := g.PartialTurn()
if partialTurn != 0 {
total := g.PartialTime()
switch partialTurn {
case 1:
total += g.Player1.Inactive
case 2:
total += g.Player2.Inactive
}
if total >= inactiveLimit {
g.inactive = partialTurn
g.SetPartialHandled(true)
if !g.terminated() {
var player *serverClient
var opponent *serverClient
switch partialTurn {
case 1:
player = g.client1
opponent = g.client2
case 2:
player = g.client2
opponent = g.client1
}
if player != nil {
player.sendNotice("You have been inactive for more than ten minutes. If your opponent leaves the match they will receive a win.")
}
if opponent != nil {
opponent.sendNotice("Your opponent has been inactive for more than ten minutes. You may continue playing or leave the match at any time and receive a win.")
}
}
}
}
}
if !g.terminated() {
s.games[i] = g
i++
} else if g.forefeit != 0 && g.Winner == 0 {
g.Winner = 1
if g.forefeit == 1 {
g.Winner = 2
} else if g.Winner == 0 && (g.inactive != 0 || g.forefeit != 0) {
if g.inactive != 0 {
g.Winner = 1
if g.inactive == 1 {
g.Winner = 2
}
} else {
g.Winner = 1
if g.forefeit == 1 {
g.Winner = 2
}
}
err := recordMatchResult(g, matchTypeCasual)
if err != nil {

View file

@ -519,6 +519,7 @@ COMMANDS:
}
clientGame.DoubleOffered = true
clientGame.NextPartialTurn(opponent.playerNumber)
cmd.client.sendNotice(fmt.Sprintf("Double offered to opponent (%d points).", clientGame.DoubleValue*2))
clientGame.opponent(cmd.client).sendNotice(fmt.Sprintf("%s offers a double (%d points).", cmd.client.name, clientGame.DoubleValue*2))
@ -552,6 +553,8 @@ COMMANDS:
continue
}
clientGame.NextPartialTurn(opponent.playerNumber)
cmd.client.sendNotice("Declined double offer")
clientGame.opponent(cmd.client).sendNotice(fmt.Sprintf("%s declined double offer.", cmd.client.name))
@ -734,6 +737,8 @@ COMMANDS:
}
}
clientGame.NextPartialTurn(clientGame.Turn)
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
@ -898,6 +903,7 @@ COMMANDS:
clientGame.DoubleOffered = false
clientGame.DoubleValue = clientGame.DoubleValue * 2
clientGame.DoublePlayer = cmd.client.playerNumber
clientGame.NextPartialTurn(opponent.playerNumber)
cmd.client.sendNotice("Accepted double.")
opponent.sendNotice(fmt.Sprintf("%s accepted double.", cmd.client.name))

View file

@ -1,11 +1,12 @@
package bgammon
type Player struct {
Number int8 // 1 black, 2 white
Name string
Rating int
Points int8
Entered bool // Whether all checkers have entered the board. (Acey-deucey)
Number int8 // 1 black, 2 white
Name string
Rating int
Points int8
Entered bool // Whether all checkers have entered the board. (Acey-deucey)
Inactive int // Inactive time. (Seconds)
}
func NewPlayer(number int8) Player {