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 {