Skip to content

Commit dce0428

Browse files
committed
update: Score calculation
1 parent 2e7c329 commit dce0428

File tree

4 files changed

+102
-24
lines changed

4 files changed

+102
-24
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ require (
2424
)
2525

2626
require (
27+
github.com/Knetic/govaluate v3.0.0+incompatible // indirect
2728
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
2829
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
2930
github.com/fsnotify/fsnotify v1.7.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/Knetic/govaluate v3.0.0+incompatible h1:7o6+MAPhYTCF0+fdvoz1xDedhRb4f6s9Tn1Tt7/WTEg=
2+
github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
13
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
24
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
35
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

server/router/api/v1/flag_service.go

Lines changed: 93 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ package v1
1717
import (
1818
"fmt"
1919
"log/slog"
20+
"math"
2021
"time"
2122

23+
"github.com/Knetic/govaluate"
2224
"github.com/labstack/echo/v4"
2325
"rina.icu/hoshino/internal/util"
2426
"rina.icu/hoshino/server/context"
@@ -51,33 +53,32 @@ func anticheatCheck(_ *echo.Context, s *store.Store,
5153
flag, err := s.GetFlagByChallenge(flag, challenge)
5254
if err != nil {
5355
slog.Error(fmt.Sprintf("Failed to get flag: %s ", err.Error()))
54-
return true, CheatReasonNone
55-
}
56+
} else {
57+
if flag.Team.ID != team.ID {
58+
event := store.GameEvent{
59+
Content: fmt.Sprintf("Team `%s` shared the flag `%s` with team `%s`", team.Name, flag.Flag, flag.Team.Name),
60+
Game: challenge.Game,
61+
Challenge: challenge,
62+
RelatedTeams: []*store.Team{team, flag.Team},
63+
Visibility: true,
64+
Type: store.GameEventTypeCheatDetected,
65+
}
5666

57-
if flag.Team.ID != team.ID {
58-
event := store.GameEvent{
59-
Content: fmt.Sprintf("Team `%s` shared the flag `%s` with team `%s`", team.Name, flag.Flag, flag.Team.Name),
60-
Game: challenge.Game,
61-
Challenge: challenge,
62-
RelatedTeams: []*store.Team{team, flag.Team},
63-
Visibility: true,
64-
Type: store.GameEventTypeCheatDetected,
65-
}
67+
flag.State = store.FlagCheated
68+
s.UpdateFlag(flag)
6669

67-
flag.State = store.FlagCheated
68-
s.UpdateFlag(flag)
70+
if challenge.Game.AutoBan {
71+
// ban the team instantly
72+
team.Banned = true
73+
s.UpdateTeam(team)
74+
} else {
75+
// log the cheat silently
76+
event.Visibility = false
77+
}
6978

70-
if challenge.Game.AutoBan {
71-
// ban the team instantly
72-
team.Banned = true
73-
s.UpdateTeam(team)
74-
} else {
75-
// log the cheat silently
76-
event.Visibility = false
79+
s.CreateGameEvent(&event)
80+
return false, CheatReasonSharingFlag
7781
}
78-
79-
s.CreateGameEvent(&event)
80-
return false, CheatReasonSharingFlag
8182
}
8283
} else {
8384
attachments, err := s.GetAttachmentsByChallenge(challenge)
@@ -158,6 +159,71 @@ func anticheatCheck(_ *echo.Context, s *store.Store,
158159
return true, CheatReasonNone
159160
}
160161

162+
func updateScore(s *store.Store, challenge *store.Challenge) {
163+
// update the score of the challenge
164+
solvedFlags, err := s.GetSolvedFlagsByChallenge(challenge)
165+
if err != nil {
166+
slog.Error(fmt.Sprintf("Failed to get solved flags: %s ", err.Error()))
167+
return
168+
}
169+
170+
solvedRate := float64(len(solvedFlags)) / float64(len(challenge.Game.GetTeams(s)))
171+
lossRate := float64(len(solvedFlags)-1) / float64(len(challenge.Game.GetTeams(s)))
172+
exponentialScore := float64(challenge.Score) * math.Exp((float64(challenge.Difficulty)-2)*lossRate)
173+
parameters := make(map[string]interface{})
174+
parameters["original_score"] = challenge.Score
175+
parameters["solved_count"] = len(solvedFlags)
176+
parameters["team_count"] = len(challenge.Game.GetTeams(s))
177+
parameters["difficulty"] = challenge.Difficulty
178+
parameters["loss_rate"] = lossRate
179+
parameters["solved_rate"] = solvedRate
180+
parameters["unsolved_rate"] = 1 - solvedRate
181+
parameters["linear_score"] = float64(challenge.Score) * (1 - lossRate)
182+
parameters["exponential_score"] = exponentialScore
183+
184+
slog.Info(fmt.Sprintf("Original score: %d, Solved count: %d, Team count: %d, Difficulty: %f, Loss rate: %f, Solved rate: %f, Unsolved rate: %f, Linear score: %f, Exponential score: %f", challenge.Score, len(solvedFlags), len(challenge.Game.GetTeams(s)), challenge.Difficulty, lossRate, solvedRate, 1-solvedRate, float64(challenge.Score)*(1-lossRate), exponentialScore))
185+
186+
functions := map[string]govaluate.ExpressionFunction{
187+
"max": func(args ...interface{}) (interface{}, error) {
188+
return math.Max(args[0].(float64), args[1].(float64)), nil
189+
},
190+
"min": func(args ...interface{}) (interface{}, error) {
191+
return math.Min(args[0].(float64), args[1].(float64)), nil
192+
},
193+
"exponential_score_with_top3_bonus": func(args ...interface{}) (interface{}, error) {
194+
order := args[0].(uint32)
195+
rate1 := args[1].(float64)
196+
rate2 := args[2].(float64)
197+
rate3 := args[3].(float64)
198+
if order == 1 {
199+
return exponentialScore * rate1, nil
200+
} else if order == 2 {
201+
return exponentialScore * rate2, nil
202+
} else if order == 3 {
203+
return exponentialScore * rate3, nil
204+
}
205+
return exponentialScore, nil
206+
},
207+
}
208+
209+
expression, err := govaluate.NewEvaluableExpressionWithFunctions(challenge.ScoreFormula, functions)
210+
211+
actualOrder := 1
212+
for _, flag := range solvedFlags {
213+
if flag.State == 2 && challenge.Game.AutoBan {
214+
continue
215+
}
216+
217+
parameters["order"] = actualOrder
218+
219+
result, _ := expression.Evaluate(parameters)
220+
score := math.Round(result.(float64))
221+
flag.Score = int(score)
222+
s.UpdateFlag(flag)
223+
actualOrder++
224+
}
225+
}
226+
161227
func SubmitFlag(c echo.Context) error {
162228
ctx := c.(*context.CustomContext)
163229

@@ -224,6 +290,7 @@ func SubmitFlag(c echo.Context) error {
224290
ok, _ := anticheatCheck(&c, ctx.Store, payload.Flag, team, challenge)
225291
if !ok {
226292
storedFlag.State = store.FlagCheated
293+
storedFlag.SolvedAt = time.Now().UnixMilli()
227294
ctx.Store.UpdateFlag(storedFlag)
228295

229296
if challenge.Game.AutoBan {
@@ -247,5 +314,8 @@ func SubmitFlag(c echo.Context) error {
247314
ctx.Store.UpdateFlag(storedFlag)
248315
return Failed(&c, "Flag is incorrect")
249316
}
317+
318+
go updateScore(ctx.Store, challenge)
319+
250320
return OK(&c)
251321
}

store/flag.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ type Flag struct {
3838
SolvedAt int64 `gorm:"default:0"`
3939

4040
// Score is the score of the flag
41-
// The score will NOT be caculated in some game mode.
4241
Score int `gorm:"default:-1"`
4342

4443
// ChallengeID is the ID of the challenge that the flag belongs to
@@ -89,3 +88,9 @@ func (s *Store) GetFlagByChallengeAndTeam(challenge *Challenge, team *Team) (*Fl
8988
err := s.db.Preload("Team").Preload("Challenge").Where("challenge_id = ? AND team_id = ?", challenge.ID, team.ID).Order("ID DESC").First(&flag).Error
9089
return &flag, err
9190
}
91+
92+
func (s *Store) GetSolvedFlagsByChallenge(challenge *Challenge) ([]*Flag, error) {
93+
var flags []*Flag
94+
err := s.db.Preload("Team").Preload("Challenge").Where("challenge_id = ? AND state >= 1", challenge.ID).Order("solved_at ASC").Find(&flags).Error
95+
return flags, err
96+
}

0 commit comments

Comments
 (0)