Skip to content

Commit e8c57a6

Browse files
committed
Prepare backend and GUI for multiple scoring algorithms
1 parent 242c800 commit e8c57a6

File tree

9 files changed

+142
-63
lines changed

9 files changed

+142
-63
lines changed

internal/api/createparse.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ func ParseLanguage(value string) (*game.LanguageData, string, error) {
3636
return nil, "", errors.New("the given language doesn't match any supported language")
3737
}
3838

39+
func ParseScoreCalculation(value string) (game.ScoreCalculation, error) {
40+
toLower := strings.ToLower(strings.TrimSpace(value))
41+
switch toLower {
42+
case "", "chill":
43+
return &game.ChillScoring{}, nil
44+
}
45+
46+
return nil, errors.New("the given score calculation doesn't match any supported algorithm")
47+
}
48+
3949
// ParseDrawingTime checks whether the given value is an integer between
4050
// the lower and upper bound of drawing time. All other invalid
4151
// input, including empty strings, will return an error.

internal/api/v1.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ func (handler *V1Handler) postLobby(writer http.ResponseWriter, request *http.Re
104104
}
105105
}
106106

107+
scoreCalculation, scoreCalculationInvalid := ParseScoreCalculation(request.Form.Get("score_calculation"))
107108
languageData, languageKey, languageInvalid := ParseLanguage(request.Form.Get("language"))
108109
drawingTime, drawingTimeInvalid := ParseDrawingTime(handler.cfg, request.Form.Get("drawing_time"))
109110
rounds, roundsInvalid := ParseRounds(handler.cfg, request.Form.Get("rounds"))
@@ -120,6 +121,9 @@ func (handler *V1Handler) postLobby(writer http.ResponseWriter, request *http.Re
120121
}
121122
customWords, customWordsInvalid := ParseCustomWords(lowercaser, request.Form.Get("custom_words"))
122123

124+
if scoreCalculationInvalid != nil {
125+
requestErrors = append(requestErrors, scoreCalculationInvalid.Error())
126+
}
123127
if languageInvalid != nil {
124128
requestErrors = append(requestErrors, languageInvalid.Error())
125129
}
@@ -153,7 +157,7 @@ func (handler *V1Handler) postLobby(writer http.ResponseWriter, request *http.Re
153157
playerName := GetPlayername(request)
154158
player, lobby, err := game.CreateLobby(desiredLobbyId, playerName,
155159
languageKey, publicLobby, drawingTime, rounds, maxPlayers,
156-
customWordsPerTurn, clientsPerIPLimit, customWords)
160+
customWordsPerTurn, clientsPerIPLimit, customWords, scoreCalculation)
157161
if err != nil {
158162
http.Error(writer, err.Error(), http.StatusBadRequest)
159163
return

internal/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type LobbySettingDefaults struct {
2222
CustomWordsPerTurn string `env:"CUSTOM_WORDS_PER_TURN"`
2323
ClientsPerIPLimit string `env:"CLIENTS_PER_IP_LIMIT"`
2424
Language string `env:"LANGUAGE"`
25+
ScoreCalculation string `env:"SCORE_CALCULATION"`
2526
}
2627

2728
type CORS struct {
@@ -70,6 +71,7 @@ var Default = Config{
7071
CustomWordsPerTurn: "3",
7172
ClientsPerIPLimit: "2",
7273
Language: "english",
74+
ScoreCalculation: "chill",
7375
},
7476
LobbySettingBounds: game.SettingBounds{
7577
MinDrawingTime: 60,

internal/frontend/create.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ func (handler *SSRHandler) createDefaultLobbyCreatePageData() *LobbyCreatePageDa
8080
BasePageConfig: handler.basePageConfig,
8181
SettingBounds: handler.cfg.LobbySettingBounds,
8282
Languages: game.SupportedLanguages,
83+
ScoreCalculations: game.SupportedScoreCalculations,
8384
LobbySettingDefaults: handler.cfg.LobbySettingDefaults,
8485
}
8586
}
@@ -90,10 +91,11 @@ type LobbyCreatePageData struct {
9091
config.LobbySettingDefaults
9192
game.SettingBounds
9293

93-
Translation translations.Translation
94-
Locale string
95-
Errors []string
96-
Languages map[string]string
94+
Translation translations.Translation
95+
Locale string
96+
Errors []string
97+
Languages map[string]string
98+
ScoreCalculations []string
9799
}
98100

99101
// ssrCreateLobby allows creating a lobby, optionally returning errors that
@@ -104,6 +106,7 @@ func (handler *SSRHandler) ssrCreateLobby(writer http.ResponseWriter, request *h
104106
return
105107
}
106108

109+
scoreCalculation, scoreCalculationInvalid := api.ParseScoreCalculation(request.Form.Get("score_calculation"))
107110
languageData, languageKey, languageInvalid := api.ParseLanguage(request.Form.Get("language"))
108111
drawingTime, drawingTimeInvalid := api.ParseDrawingTime(handler.cfg, request.Form.Get("drawing_time"))
109112
rounds, roundsInvalid := api.ParseRounds(handler.cfg, request.Form.Get("rounds"))
@@ -133,10 +136,14 @@ func (handler *SSRHandler) ssrCreateLobby(writer http.ResponseWriter, request *h
133136
CustomWordsPerTurn: request.Form.Get("custom_words_per_turn"),
134137
ClientsPerIPLimit: request.Form.Get("clients_per_ip_limit"),
135138
Language: request.Form.Get("language"),
139+
ScoreCalculation: request.Form.Get("score_calculation"),
136140
},
137141
Languages: game.SupportedLanguages,
138142
}
139143

144+
if scoreCalculationInvalid != nil {
145+
pageData.Errors = append(pageData.Errors, scoreCalculationInvalid.Error())
146+
}
140147
if languageInvalid != nil {
141148
pageData.Errors = append(pageData.Errors, languageInvalid.Error())
142149
}
@@ -178,7 +185,7 @@ func (handler *SSRHandler) ssrCreateLobby(writer http.ResponseWriter, request *h
178185

179186
player, lobby, err := game.CreateLobby(uuid.Nil, playerName, languageKey,
180187
publicLobby, drawingTime, rounds, maxPlayers, customWordsPerTurn,
181-
clientsPerIPLimit, customWords)
188+
clientsPerIPLimit, customWords, scoreCalculation)
182189
if err != nil {
183190
pageData.Errors = append(pageData.Errors, err.Error())
184191
if err := pageTemplates.ExecuteTemplate(writer, "lobby-create-page", pageData); err != nil {

internal/frontend/templates/lobby_create.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,17 @@
4646
<option value="{{$k}}" {{if eq $k $language}}selected="selected" {{end}}>{{$v}}</option>
4747
{{end}}
4848
</select>
49+
<label class="lobby-create-label" for="score_calculation">
50+
{{.Translation.Get "score-calculation"}}
51+
</label>
52+
<select class="input-item" name="score_calculation" id="score_calculation" placeholder="Choose
53+
how player scores are determined">
54+
{{$scoreCalculation := .ScoreCalculation}}
55+
{{range .ScoreCalculations}}
56+
<option value="{{.}}" {{if eq . $scoreCalculation}}selected="selected" {{end}}>{{ . }}
57+
</option>
58+
{{end}}
59+
</select>
4960
<label class="lobby-create-label" for="drawing_time">
5061
{{.Translation.Get "drawing-time-setting"}}
5162
</label>

internal/game/data.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ type Lobby struct {
4141
// creator is the player that opened a lobby. Initially creator and owner
4242
// are set to the same player. While the owner can change throughout the
4343
// game, the creator can't.
44-
creator *Player
44+
creator *Player
45+
scoreCalculation ScoreCalculation
4546
// CurrentWord represents the word that was last selected. If no word has
4647
// been selected yet or the round is already over, this should be empty.
4748
CurrentWord string

internal/game/lobby.go

Lines changed: 84 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ import (
2020
"github.com/gofrs/uuid/v5"
2121
)
2222

23+
var SupportedScoreCalculations = []string{
24+
"chill",
25+
}
26+
2327
var SupportedLanguages = map[string]string{
2428
"english_gb": "English (GB)",
2529
"english": "English (US)",
@@ -282,9 +286,7 @@ func handleMessage(message string, sender *Player, lobby *Lobby) {
282286
switch CheckGuess(normInput, normSearched) {
283287
case EqualGuess:
284288
{
285-
secondsLeft := int(lobby.roundEndTime/1000 - time.Now().UTC().Unix())
286-
287-
sender.LastScore = calculateGuesserScore(lobby.hintCount, lobby.hintsLeft, secondsLeft, lobby.DrawingTime)
289+
sender.LastScore = lobby.calculateGuesserScore()
288290
sender.Score += sender.LastScore
289291

290292
sender.State = Standby
@@ -321,24 +323,6 @@ func (lobby *Lobby) wasLastDrawEventFill() bool {
321323
return isFillEvent
322324
}
323325

324-
func calculateGuesserScore(hintCount, hintsLeft, secondsLeft, drawingTime int) int {
325-
// The base score is based on the general time taken.
326-
// The formula here represents an exponential decline based on the time taken.
327-
// This way fast players get more points, however not a lot more.
328-
// The bonus gained by guessing before hints are shown is therefore still somewhat relevant.
329-
declineFactor := 1.0 / float64(drawingTime)
330-
baseScore := int(maxBaseScore * math.Pow(1.0-declineFactor, float64(drawingTime-secondsLeft)))
331-
332-
// Prevent zero division panic. This could happen with two letter words.
333-
if hintCount <= 0 {
334-
return baseScore
335-
}
336-
337-
// If all hints are shown, or the word is too short to show hints, the
338-
// calculation will basically always be baseScore + 0.
339-
return baseScore + hintsLeft*(maxHintBonusScore/hintCount)
340-
}
341-
342326
func (lobby *Lobby) isAnyoneStillGuessing() bool {
343327
for _, otherPlayer := range lobby.players {
344328
if otherPlayer.State == Guessing && otherPlayer.Connected {
@@ -597,34 +581,12 @@ func handleNameChangeEvent(caller *Player, lobby *Lobby, name string) {
597581
}
598582
}
599583

600-
func (lobby *Lobby) calculateDrawerScore() int {
601-
// The drawer can get points even if disconnected. But if they are
602-
// connected, we need to ignore them when calculating their score.
603-
var (
604-
playerCount int
605-
scoreSum int
606-
)
607-
for _, player := range lobby.GetPlayers() {
608-
if player.State != Drawing &&
609-
// Switch to spectating is only possible after score calculation, so
610-
// this can't be used to manipulate score.
611-
player.State != Spectating &&
612-
// If the player has guessed, we want to take them into account,
613-
// even if they aren't connected anymore. If the player is
614-
// connected, but hasn't guessed, it is still as well, as the
615-
// drawing must've not been good enough to be guessable.
616-
(player.Connected || player.LastScore > 0) {
617-
scoreSum += player.LastScore
618-
playerCount++
619-
}
620-
}
621-
622-
var averageScore int
623-
if playerCount > 0 {
624-
averageScore = scoreSum / playerCount
625-
}
584+
func (lobby *Lobby) calculateGuesserScore() int {
585+
return lobby.scoreCalculation.CalculateGuesserScore(lobby)
586+
}
626587

627-
return averageScore
588+
func (lobby *Lobby) calculateDrawerScore() int {
589+
return lobby.scoreCalculation.CalculateDrawerScore(lobby)
628590
}
629591

630592
// advanceLobbyPredefineDrawer is required in cases where the drawer is removed
@@ -946,6 +908,7 @@ func CreateLobby(
946908
publicLobby bool,
947909
drawingTime, rounds, maxPlayers, customWordsPerTurn, clientsPerIPLimit int,
948910
customWords []string,
911+
scoringCalculation ScoreCalculation,
949912
) (*Player, *Lobby, error) {
950913
if desiredLobbyId == uuid.Nil {
951914
desiredLobbyId = uuid.Must(uuid.NewV4())
@@ -960,9 +923,10 @@ func CreateLobby(
960923
ClientsPerIPLimit: clientsPerIPLimit,
961924
Public: publicLobby,
962925
},
963-
CustomWords: customWords,
964-
currentDrawing: make([]any, 0),
965-
State: Unstarted,
926+
CustomWords: customWords,
927+
currentDrawing: make([]any, 0),
928+
State: Unstarted,
929+
scoreCalculation: scoringCalculation,
966930
}
967931

968932
if len(customWords) > 1 {
@@ -1139,3 +1103,72 @@ func (lobby *Lobby) Shutdown() {
11391103

11401104
lobby.Broadcast(&EventTypeOnly{Type: EventTypeShutdown})
11411105
}
1106+
1107+
// ScoreCalculation allows having different scoring systems for
1108+
type ScoreCalculation interface {
1109+
Identifier() string
1110+
CalculateGuesserScore(*Lobby) int
1111+
CalculateDrawerScore(*Lobby) int
1112+
}
1113+
1114+
type ChillScoring struct{}
1115+
1116+
func (s *ChillScoring) Identifier() string {
1117+
return "chill"
1118+
}
1119+
1120+
func (s *ChillScoring) CalculateGuesserScore(lobby *Lobby) int {
1121+
return s.calculateGuesserScore(lobby.hintCount, lobby.hintsLeft, lobby.DrawingTime, lobby.roundEndTime)
1122+
}
1123+
1124+
func (s *ChillScoring) calculateGuesserScore(
1125+
hintCount, hintsLeft, drawingTime int,
1126+
roundEndTimeMillis int64,
1127+
) int {
1128+
secondsLeft := int(roundEndTimeMillis/1000 - time.Now().UTC().Unix())
1129+
1130+
// The base score is based on the general time taken.
1131+
// The formula here represents an exponential decline based on the time taken.
1132+
// This way fast players get more points, however not a lot more.
1133+
// The bonus gained by guessing before hints are shown is therefore still somewhat relevant.
1134+
declineFactor := 1.0 / float64(drawingTime)
1135+
baseScore := int(maxBaseScore * math.Pow(1.0-declineFactor, float64(drawingTime-secondsLeft)))
1136+
1137+
// Prevent zero division panic. This could happen with two letter words.
1138+
if hintCount <= 0 {
1139+
return baseScore
1140+
}
1141+
1142+
// If all hints are shown, or the word is too short to show hints, the
1143+
// calculation will basically always be baseScore + 0.
1144+
return baseScore + hintsLeft*(maxHintBonusScore/hintCount)
1145+
}
1146+
1147+
func (s *ChillScoring) CalculateDrawerScore(lobby *Lobby) int {
1148+
// The drawer can get points even if disconnected. But if they are
1149+
// connected, we need to ignore them when calculating their score.
1150+
var (
1151+
playerCount int
1152+
scoreSum int
1153+
)
1154+
for _, player := range lobby.GetPlayers() {
1155+
if player.State != Drawing &&
1156+
// Switch to spectating is only possible after score calculation, so
1157+
// this can't be used to manipulate score.
1158+
player.State != Spectating &&
1159+
// If the player has guessed, we want to take them into account,
1160+
// even if they aren't connected anymore. If the player is
1161+
// connected, but hasn't guessed, it is still as well, as the
1162+
// drawing must've not been good enough to be guessable.
1163+
(player.Connected || player.LastScore > 0) {
1164+
scoreSum += player.LastScore
1165+
playerCount++
1166+
}
1167+
}
1168+
1169+
if playerCount > 0 {
1170+
return scoreSum / playerCount
1171+
}
1172+
1173+
return 0
1174+
}

0 commit comments

Comments
 (0)