From 28ba643bf9942099f41b849d45f82ee8e06c7759 Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Thu, 16 Nov 2023 16:50:11 -0800 Subject: [PATCH] Support spectating matches Resolves #3. --- cmd/bgammon-server/game.go | 56 +++++++++++++++++++++++++++++------- cmd/bgammon-server/server.go | 37 +++++++++++++----------- gamestate.go | 11 +++---- 3 files changed, 72 insertions(+), 32 deletions(-) diff --git a/cmd/bgammon-server/game.go b/cmd/bgammon-server/game.go index 79bfbbe..7a4c358 100644 --- a/cmd/bgammon-server/game.go +++ b/cmd/bgammon-server/game.go @@ -16,6 +16,7 @@ type serverGame struct { password []byte client1 *serverClient client2 *serverClient + spectators []*serverClient allowed1 []byte allowed2 []byte rematch int @@ -79,6 +80,9 @@ func (g *serverGame) sendBoard(client *serverClient) { Available: g.LegalMoves(false), }, } + if g.client1 != client && g.client2 != client { + ev.Spectating = true + } // Reverse spaces for white. if client.playerNumber == 2 { @@ -160,11 +164,33 @@ func (g *serverGame) eachClient(f func(client *serverClient)) { if g.client2 != nil { f(g.client2) } + for _, spectator := range g.spectators { + f(spectator) + } } -func (g *serverGame) addClient(client *serverClient) (bool, string) { +func (g *serverGame) addClient(client *serverClient) (spectator bool) { if g.allowed1 != nil && !bytes.Equal(client.name, g.allowed1) && !bytes.Equal(client.name, g.allowed2) { - return false, "Match has already started." + 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) + return spectator } var playerNumber int @@ -201,8 +227,6 @@ func (g *serverGame) addClient(client *serverClient) (bool, string) { } }() switch { - case g.client1 != nil && g.client2 != nil: - // Do not assign player number. case g.client1 != nil: g.client2 = client g.Player2.Name = string(client.name) @@ -226,13 +250,7 @@ func (g *serverGame) addClient(client *serverClient) (bool, string) { playerNumber = 2 } } - - ok := playerNumber != 0 - var reason string - if !ok { - reason = "Match is full." - } - return ok, reason + return spectator } func (g *serverGame) removeClient(client *serverClient) { @@ -275,6 +293,22 @@ func (g *serverGame) removeClient(client *serverClient) { g.Player2.Name = "" 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) + } + + client.playerNumber = 0 + return + } + } return } } diff --git a/cmd/bgammon-server/server.go b/cmd/bgammon-server/server.go index f110891..da345f2 100644 --- a/cmd/bgammon-server/server.go +++ b/cmd/bgammon-server/server.go @@ -334,6 +334,11 @@ func (s *server) gameByClient(c *serverClient) *serverGame { if g.client1 == c || g.client2 == c { return g } + for _, spec := range g.spectators { + if spec == c { + return g + } + } } return nil } @@ -440,10 +445,8 @@ COMMANDS: rejoin = g.rejoin2 } if rejoin { - ok, _ := g.addClient(cmd.client) - if ok { - cmd.client.sendNotice(fmt.Sprintf("Rejoined match: %s", g.name)) - } + g.addClient(cmd.client) + cmd.client.sendNotice(fmt.Sprintf("Rejoined match: %s", g.name)) } } s.gamesLock.RUnlock() @@ -455,6 +458,15 @@ COMMANDS: } clientGame := s.gameByClient(cmd.client) + if clientGame != nil && clientGame.client1 != cmd.client && clientGame.client2 != cmd.client { + switch keyword { + case bgammon.CommandHelp, "h", bgammon.CommandJSON, bgammon.CommandList, "ls", bgammon.CommandBoard, "b", bgammon.CommandLeave, "l", bgammon.CommandDisconnect, bgammon.CommandPong: + // These commands are allowed to be used by spectators. + default: + cmd.client.sendNotice("Command ignored: You are spectating this match.") + continue + } + } switch keyword { case bgammon.CommandHelp, "h": @@ -573,10 +585,7 @@ COMMANDS: g.name = gameName g.Points = points g.password = gamePassword - ok, reason := g.addClient(cmd.client) - if !ok { - log.Panicf("failed to add client to newly created game %+v %+v: %s", g, cmd.client, reason) - } + g.addClient(cmd.client) s.gamesLock.Lock() s.games = append(s.games, g) @@ -651,15 +660,11 @@ COMMANDS: s.gamesLock.Unlock() continue COMMANDS } - ok, reason := g.addClient(cmd.client) + spectator := g.addClient(cmd.client) s.gamesLock.Unlock() - - if !ok { - cmd.client.sendEvent(&bgammon.EventFailedJoin{ - Reason: reason, - }) - } else { - cmd.client.sendNotice(fmt.Sprintf("Joined match: %s", g.name)) + cmd.client.sendNotice(fmt.Sprintf("Joined match: %s", g.name)) + if spectator { + cmd.client.sendNotice("You are spectating this match. Chat messages are not relayed.") } continue COMMANDS } diff --git a/gamestate.go b/gamestate.go index 900cda8..954fba0 100644 --- a/gamestate.go +++ b/gamestate.go @@ -8,6 +8,7 @@ type GameState struct { *Game PlayerNumber int Available [][]int // Legal moves. + Spectating bool } func (g *GameState) OpponentPlayer() Player { @@ -75,7 +76,7 @@ func (g *GameState) Pips(player int) int { // MayDouble returns whether the player may send the 'double' command. func (g *GameState) MayDouble() bool { - if g.Winner != 0 { + if g.Spectating || g.Winner != 0 { return false } return g.Points != 1 && g.Turn != 0 && g.Turn == g.PlayerNumber && g.Roll1 == 0 && !g.DoubleOffered && (g.DoublePlayer == 0 || g.DoublePlayer == g.PlayerNumber) @@ -83,7 +84,7 @@ func (g *GameState) MayDouble() bool { // MayRoll returns whether the player may send the 'roll' command. func (g *GameState) MayRoll() bool { - if g.Winner != 0 || g.DoubleOffered { + if g.Spectating || g.Winner != 0 || g.DoubleOffered { return false } switch g.Turn { @@ -106,7 +107,7 @@ func (g *GameState) MayRoll() bool { // MayOK returns whether the player may send the 'ok' command. func (g *GameState) MayOK() bool { - if g.Winner != 0 { + if g.Spectating || g.Winner != 0 { return false } else if g.Turn != 0 && g.Turn != g.PlayerNumber && g.DoubleOffered { return true @@ -116,7 +117,7 @@ func (g *GameState) MayOK() bool { // MayResign returns whether the player may send the 'resign' command. func (g *GameState) MayResign() bool { - if g.Winner != 0 { + if g.Spectating || g.Winner != 0 { return false } return g.Turn != 0 && g.Turn != g.PlayerNumber && g.DoubleOffered @@ -124,7 +125,7 @@ func (g *GameState) MayResign() bool { // MayReset returns whether the player may send the 'reset' command. func (g *GameState) MayReset() bool { - if g.Winner != 0 { + if g.Spectating || g.Winner != 0 { return false } return g.Turn != 0 && g.Turn == g.PlayerNumber && len(g.Moves) > 0