Skip to content

Commit

Permalink
Spectator mode WIP
Browse files Browse the repository at this point in the history
Fixes #201
  • Loading branch information
Bios-Marcel committed Aug 20, 2023
1 parent ac0b188 commit 337a660
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 21 deletions.
17 changes: 15 additions & 2 deletions internal/frontend/templates/lobby.html
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,17 @@
</div>
</div>

<div id="spectator-choice-dialog" class="center-dialog">
<span class="dialog-title">TODO</span>
<div class="center-dialog-content">
<!-- FIXME New class / More generic class? -->
<div class="word-button-container">
<button class="dialog-button" onclick="participate()"></button>
<button class="dialog-button" onclick="spectate()"></button>
</div>
</div>
</div>

<div id="unstarted-dialog" class="center-dialog">
<span class="dialog-title">{{.Translation.Get "game-not-started-title"}}</span>
<div class="center-dialog-content">
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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;
}

Expand Down
7 changes: 4 additions & 3 deletions internal/game/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
54 changes: 38 additions & 16 deletions internal/game/lobby.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -635,7 +660,6 @@ func advanceLobbyPredefineDrawer(lobby *Lobby, roundOver bool, newDrawer *Player
})
}

// Omit rest of events, since we don't need to advance.
return
}

Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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
}
}

Expand Down
5 changes: 5 additions & 0 deletions internal/game/shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 337a660

Please sign in to comment.