From 337a6605b9d2da58b4c50356a1f4e92bf3c05445 Mon Sep 17 00:00:00 2001 From: Marcel Schramm Date: Sat, 19 Aug 2023 20:33:52 +0200 Subject: [PATCH] Spectator mode WIP Fixes #201 --- internal/frontend/templates/lobby.html | 17 +++++++- internal/game/data.go | 7 ++-- internal/game/lobby.go | 54 ++++++++++++++++++-------- internal/game/shared.go | 5 +++ 4 files changed, 62 insertions(+), 21 deletions(-) diff --git a/internal/frontend/templates/lobby.html b/internal/frontend/templates/lobby.html index 90ec0c54..653ed17c 100644 --- a/internal/frontend/templates/lobby.html +++ b/internal/frontend/templates/lobby.html @@ -121,6 +121,17 @@ +
+ TODO +
+ +
+ + +
+
+
+
{{.Translation.Get "game-not-started-title"}}
@@ -507,6 +518,7 @@ const centerDialogs = document.getElementById("center-dialogs"); + const spectatorChoiceDialog = document.getElementById("spectator-choice-dialog"); const unstartedDialog = document.getElementById("unstarted-dialog"); const waitChooseDialog = document.getElementById("waitchoose-dialog"); const waitChooseDrawerSpan = document.getElementById("waitchoose-drawer"); @@ -1386,8 +1398,9 @@ playerContainer.innerHTML = ""; cachedPlayers = players; players.forEach(player => { - //We don't wanna show the disconnected players. - if (!player.connected) { + // We don't wanna show the disconnected players or spectators. + // the UI is cluttered enough as is. + if (!player.connected || player.state === "spectator") { return; } diff --git a/internal/game/data.go b/internal/game/data.go index 6fc4484a..a40cd5af 100644 --- a/internal/game/data.go +++ b/internal/game/data.go @@ -140,9 +140,10 @@ func (player *Player) GetUserSession() uuid.UUID { type PlayerState string const ( - Guessing PlayerState = "guessing" - Drawing PlayerState = "drawing" - Standby PlayerState = "standby" + Guessing PlayerState = "guessing" + Drawing PlayerState = "drawing" + Standby PlayerState = "standby" + Spectator PlayerState = "spectator" ) // GetPlayer searches for a player, identifying them by usersession. diff --git a/internal/game/lobby.go b/internal/game/lobby.go index 0cc2b27a..b4aa048e 100644 --- a/internal/game/lobby.go +++ b/internal/game/lobby.go @@ -80,11 +80,19 @@ func (lobby *Lobby) HandleEvent(eventType string, payload []byte, player *Player lobby.mutex.Lock() defer lobby.mutex.Unlock() + if player.State == Spectator { + return nil + } + // For all followup unmarshalling of the already unmarshalled Event, we // use mapstructure instead. It's cheaper in terms of CPU usage and // memory usage. There are benchmarks to prove this in json_test.go. - if eventType == EventTypeMessage { + if eventType == "spectate" { + player.desiredState = Spectator + } else if eventType == "participate" { + player.desiredState = Guessing + } else if eventType == EventTypeMessage { var message StringDataEvent if err := easyjson.Unmarshal(payload, &message); err != nil { return fmt.Errorf("invalid data received: '%s'", string(payload)) @@ -169,7 +177,9 @@ func (lobby *Lobby) HandleEvent(eventType string, payload []byte, player *Player wordHintData := &Event{Type: EventTypeUpdateWordHint, Data: lobby.wordHints} lobby.broadcastConditional(wordHintData, IsGuessing) wordHintDataRevealed := &Event{Type: EventTypeUpdateWordHint, Data: lobby.wordHintsShown} - lobby.broadcastConditional(wordHintDataRevealed, IsNotGuessing) + lobby.broadcastConditional(wordHintDataRevealed, func(p *Player) bool { + return p.State == Drawing || p.State == Standby + }) } } else if eventType == EventTypeKickVote { if lobby.EnableVotekick { @@ -416,6 +426,10 @@ func handleKickVoteEvent(lobby *Lobby, player *Player, toKickID uuid.UUID) { } playerToKick := lobby.players[playerToKickIndex] + // Can't kick spectators + if playerToKick.State == Spectator { + return + } player.votedForKick[toKickID] = true var voteKickCount int @@ -602,6 +616,10 @@ func advanceLobbyPredefineDrawer(lobby *Lobby, roundOver bool, newDrawer *Player lobby.scoreEarnedByGuessers = 0 for _, otherPlayer := range lobby.players { + if otherPlayer.State == Spectator { + continue + } + // If the round ends and people still have guessing, that means the // "LastScore" value for the next turn has to be "no score earned". if otherPlayer.State == Guessing { @@ -612,6 +630,13 @@ func advanceLobbyPredefineDrawer(lobby *Lobby, roundOver bool, newDrawer *Player otherPlayer.State = Guessing } + for _, player := range lobby.players { + if player.desiredState != "" { + player.State = player.desiredState + player.desiredState = "" + } + } + recalculateRanks(lobby) if roundOver { @@ -635,7 +660,6 @@ func advanceLobbyPredefineDrawer(lobby *Lobby, roundOver bool, newDrawer *Player }) } - // Omit rest of events, since we don't need to advance. return } @@ -672,29 +696,31 @@ func advanceLobby(lobby *Lobby) { advanceLobbyPredefineDrawer(lobby, roundOver, newDrawer) } +func validDrawer(player *Player) bool { + return player.Connected || player.State == Drawing || player.State == Standby +} + // determineNextDrawer returns the next person that's supposed to be drawing, but // doesn't tell the lobby yet. The boolean signals whether the current round // is over. func determineNextDrawer(lobby *Lobby) (*Player, bool) { + // Attempt to take the next player in line. for index, player := range lobby.players { if player == lobby.drawer { - // If we have someone that's drawing, take the next one for i := index + 1; i < len(lobby.players); i++ { - player := lobby.players[i] - if player.Connected { + if player := lobby.players[i]; validDrawer(player) { return player, false } } - // No player below the current drawer has been found, therefore we - // fallback to our default logic at the bottom. + // Next round break } } - // We prefer the first connected player. + // Start from the top of the player list again. for _, player := range lobby.players { - if player.Connected { + if validDrawer(player) { return player, true } } @@ -794,19 +820,15 @@ func recalculateRanks(lobby *Lobby) { lastScore := math.MaxInt32 var lastRank int for _, player := range sortedPlayers { - if !player.Connected { + if !player.Connected && player.State != Spectator { continue } if player.Score < lastScore { lastRank++ - player.Rank = lastRank lastScore = player.Score - } else { - // Since the players are already sorted from high to low, we only - // have the cases higher or equal. - player.Rank = lastRank } + player.Rank = lastRank } } diff --git a/internal/game/shared.go b/internal/game/shared.go index 775409de..31cb5afc 100644 --- a/internal/game/shared.go +++ b/internal/game/shared.go @@ -204,6 +204,11 @@ type Player struct { // space for new players. The player with the oldest disconnect.Time will // get kicked. disconnectTime *time.Time + // desiredState is used for state changes between spectator and player. + // We want to prevent people from switching in and out of the play state. + // While this will allow people to skip being the drawer, it will also + // cause them to lose points for that round. + desiredState PlayerState votedForKick map[uuid.UUID]bool