diff --git a/server/frontend/pages/problem.html b/server/frontend/pages/problem.html index 4dff90f..a4aa9f2 100644 --- a/server/frontend/pages/problem.html +++ b/server/frontend/pages/problem.html @@ -39,15 +39,17 @@

Output

{{ else }} You've solved both parts! {{ end }} +

{{ end }} diff --git a/server/problem/cooldown.go b/server/problem/cooldown.go deleted file mode 100644 index 746975c..0000000 --- a/server/problem/cooldown.go +++ /dev/null @@ -1,24 +0,0 @@ -package problem - -import "time" - -// CalculateCooldownEnd calculates the end of the cooldown period for a problem -// given the total number of attempts, the time of the last submission and the -// current time. If there is no cooldown, the returned timestamp is before the -// current time, which may or may not be zero. -func CalculateCooldownEnd(totalAttempts int, lastSubmitted, now time.Time) time.Time { - const cooldownThreshold = 2 - const cooldownMultiplier = 2 - const cooldownMax = 5 * time.Minute - const cooldown = 30 * time.Second - - if totalAttempts < cooldownThreshold { - return time.Time{} - } - - n := totalAttempts - cooldownThreshold + 1 - - return lastSubmitted.Add(min( - cooldown*time.Duration(cooldownMultiplier*n), - cooldownMax)) -} diff --git a/server/problem/problem.go b/server/problem/problem.go index 3979c04..01f497e 100644 --- a/server/problem/problem.go +++ b/server/problem/problem.go @@ -20,10 +20,6 @@ import ( badgeropts "github.com/dgraph-io/badger/v4/options" ) -// PointsPerPart is the number of points awarded for solving a part of a -// problem. -const PointsPerPart = 100 - // Problem is a problem that can be solved. type Problem struct { // ID returns the unique ID of the problem. diff --git a/server/problem/problemset.go b/server/problem/problemset.go index 05d3475..61b15fb 100644 --- a/server/problem/problemset.go +++ b/server/problem/problemset.go @@ -68,6 +68,18 @@ func (p *ProblemSet) Problem(i int) *Problem { return &p.problems[i] } +// ProblemStartTime calculates the time at which the problem at the given index +// was released. If the problem set does not have a release schedule, it returns +// the zero time. +func (p *ProblemSet) ProblemStartTime(i int) time.Time { + if p.schedule == nil { + return time.Time{} + } + start := p.schedule.StartReleaseAt + delta := time.Duration(i) * p.schedule.ReleaseEvery + return start.Add(delta) +} + // TotalProblems returns the total number of problems in the set. func (p *ProblemSet) TotalProblems() int { return len(p.problems) diff --git a/server/problem/scoring.go b/server/problem/scoring.go new file mode 100644 index 0000000..1dc1e52 --- /dev/null +++ b/server/problem/scoring.go @@ -0,0 +1,53 @@ +package problem + +import ( + "math" + "time" +) + +// CalculateCooldownEnd calculates the end of the cooldown period for a problem +// given the total number of attempts, the time of the last submission and the +// current time. If there is no cooldown, the returned timestamp is before the +// current time, which may or may not be zero. +func CalculateCooldownEnd(totalAttempts int, lastSubmitted, now time.Time) time.Time { + const cooldownThreshold = 2 + const cooldownMultiplier = 2 + const cooldownMax = 5 * time.Minute + const cooldown = 30 * time.Second + + if totalAttempts < cooldownThreshold { + return time.Time{} + } + + n := totalAttempts - cooldownThreshold + 1 + + return lastSubmitted.Add(min( + cooldown*time.Duration(cooldownMultiplier*n), + cooldownMax)) +} + +const ( + // PointsPerPart is the number of points awarded for solving a part of a + // problem. + PointsPerPart = 100 + // MaxHour is the maximum hour before people get the lowest points. + MaxHour = 24 +) + +// ScalePoints scales the points for a problem's part based on the time the +// problem was started and the time the part was solved. +func ScalePoints(t, startedAt time.Time) float64 { + h := t.Sub(startedAt).Hours() + return scoreScalingFn(clamp(h/MaxHour, 0, 1)) * PointsPerPart +} + +func scoreScalingFn(x float64) float64 { + // https://www.desmos.com/calculator/22el44ng3r + f := func(x float64) float64 { return (math.Atan(-math.Pi*x+math.Pi/2) / 4) + 0.75 } + g := func(x float64) float64 { return f(x) + (1 - f(0)) } + return g(x) +} + +func clamp(x, minX, maxX float64) float64 { + return math.Max(minX, math.Min(maxX, x)) +} diff --git a/server/r_problems.go b/server/r_problems.go index f1beba6..bd65761 100644 --- a/server/r_problems.go +++ b/server/r_problems.go @@ -45,7 +45,7 @@ type problemPageData struct { frontend.ComponentContext Problem *problem.Problem Day problemDay - PointsPerPart int + PointsPerPart float64 SolvedPart1 bool SolvedPart2 bool } @@ -221,7 +221,7 @@ func (s *Server) submitProblem(w http.ResponseWriter, r *http.Request) { if correct { _, err = s.database.AddPoints(ctx, db.AddPointsParams{ TeamName: u.TeamName, - Points: problem.PointsPerPart, + Points: problem.ScalePoints(now, s.problems.ProblemStartTime(day.index())), Reason: "week of code", }) if err != nil {