Skip to content

Commit

Permalink
Add scoring v2, set Day 5 to be worth 200 pts/part
Browse files Browse the repository at this point in the history
  • Loading branch information
diamondburned committed Mar 22, 2024
1 parent f510363 commit e748071
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 56 deletions.
17 changes: 11 additions & 6 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,28 @@
"modules": [
{
"cmd": "python3 -m problems.booting-up",
"readme": "./problems/booting-up/README.md"
"readme": "./problems/booting-up/README.md",
"scoring_version": 1
},
{
"cmd": "python3 -m problems.empty-spaces",
"readme": "./problems/empty-spaces/README.md"
"readme": "./problems/empty-spaces/README.md",
"scoring_version": 1
},
{
"cmd": "python3 -m problems.crafting",
"readme": "./problems/crafting/README.md"
"readme": "./problems/crafting/README.md",
"scoring_version": 1
},
{
"cmd": "python3 -m problems.internet-connection",
"readme": "./problems/internet-connection/README.md"
"readme": "./problems/internet-connection/README.md",
"scoring_version": 1
},
{
"cmd": "false",
"readme": "./problems/_placeholder/README.md"
"cmd": "python3 -m problems.intruder-alert",
"readme": "./problems/intruder-alert/README.md",
"points_per_part": 200
}
],
"schedule": {
Expand Down
9 changes: 3 additions & 6 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"os"
"time"

"dev.acmcsuf.com/march-madness-2024/server/problem"
)

type Config struct {
Expand All @@ -20,19 +22,14 @@ type Config struct {
}

type ProblemsConfig struct {
Modules []ProblemModule `json:"modules"`
Modules []problem.ModuleConfig `json:"modules"`
Schedule struct {
Start time.Time `json:"start"`
Every Duration `json:"every"`
} `json:"schedule"`
Cooldown Duration `json:"cooldown"`
}

type ProblemModule struct {
Command string `json:"cmd"`
README string `json:"readme"`
}

type HackathonConfig struct {
StartTime time.Time `json:"start_time"`
Duration Duration `json:"duration"`
Expand Down
12 changes: 3 additions & 9 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,11 @@ func run(ctx context.Context) error {

problems := make([]problem.Problem, len(config.Problems.Modules))
for i, module := range config.Problems.Modules {
description, err := problem.ParseProblemDescriptionFile(module.README)
p, err := problem.NewProblemFromModule(module, logger)
if err != nil {
return fmt.Errorf("failed to parse README file at %q: %w", module.README, err)
return fmt.Errorf("failed to create problem from module %q: %w", module.README, err)
}

runner, err := problem.NewCommandRunner(logger.With("component", "runner"), module.Command)
if err != nil {
return fmt.Errorf("failed to create command runner %q: %w", module.Command, err)
}

problems[i] = problem.NewProblem(module.README, description, runner)
problems[i] = p
}
problem.CacheAllProblems(problems, logger.With("component", "problem_cache"))

Expand Down
11 changes: 11 additions & 0 deletions server/frontend/pages/problem.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ <h1>Day {{ .Day }}</h1>
<h2>{{ .Problem.Description.Title }}</h2>
</hgroup>

{{ if not .PPPIsDefault }}
<section>
<p>
<b>Heads up!</b>
Solving this problem will net you a total of
<b><mark>{{ .PointsPerPart }} points</mark> for each part!</b>
</p>
</section>
{{ end }}


<section class="part part1">
{{ md .Problem.Description.Part1 }}
</section>
Expand Down
3 changes: 2 additions & 1 deletion server/frontend/pages/problem_result.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ <h2>Problem Submission</h2>
{{ else }}
{{ if .Correct }}
<p>
Congratulations! Your answer is <strong>correct</strong>!
Congratulations, your answer is <strong>correct</strong>! Solving this problem nets you a
total of <b>{{ .PointsAwarded | floor }} points</b>.
<a href="/problems/{{ .Day }}">Go back to the problem here</a>, or
<a href="/leaderboard">check out the leaderboard</a>.
</p>
Expand Down
5 changes: 3 additions & 2 deletions server/frontend/pages/problems.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ <h2>Week of Code</h2>
<section>
<p>A new coding problem every day!</p>
<p>
You may get more points for solving problems <b>as soon as they are released</b>, so keep an
eye out for the next problem!
For each problem, you can earn up to <b>{{ .PointsPerPart }} points for each parts</b> you
solve. You may get more points for solving problems <b>as soon as they are released</b>, so
keep an eye out for the next problem!
</p>
</section>

Expand Down
54 changes: 47 additions & 7 deletions server/problem/problem.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@ import (
"github.com/puzpuzpuz/xsync/v3"
)

// ModuleConfig is a module that points to a problem.
type ModuleConfig struct {
Command string `json:"cmd"`
README string `json:"readme"`
ProblemConfig
}

// ProblemConfig contains optional configuration for a problem.
type ProblemConfig struct {
// PointsPerPart is the number of points awarded for each part of the problem.
// It overrides the default [PointsPerPart].
PointsPerPart float64 `json:"points_per_part,omitempty"`
// ScoringVersion is the version of the scoring function.
ScoringVersion ScoringVersion `json:"scoring_version,omitempty"`
}

// Problem is a problem that can be solved.
type Problem struct {
// ID returns the unique ID of the problem.
Expand All @@ -24,17 +40,42 @@ type Problem struct {
Description ProblemDescription

Runner
ProblemConfig
}

// NewProblem creates a new problem.
func NewProblem(id string, desc ProblemDescription, runner Runner) Problem {
func NewProblem(id string, desc ProblemDescription, runner Runner, cfg ProblemConfig) Problem {
if cfg.PointsPerPart == 0 {
cfg.PointsPerPart = PointsPerPart
}
if cfg.ScoringVersion == 0 {
cfg.ScoringVersion = latestScoreScalingVersion
}
return Problem{
ID: id,
Description: desc,
Runner: runner,
ID: id,
Description: desc,
Runner: runner,
ProblemConfig: cfg,
}
}

// NewProblemFromModule creates a new problem from a problem module.
func NewProblemFromModule(module ModuleConfig, logger *slog.Logger) (Problem, error) {
var z Problem

description, err := ParseProblemDescriptionFile(module.README)
if err != nil {
return z, fmt.Errorf("failed to parse README file at %q: %w", module.README, err)
}

runner, err := NewCommandRunner(logger.With("component", "runner"), module.Command)
if err != nil {
return z, fmt.Errorf("failed to create command runner %q: %w", module.Command, err)
}

return NewProblem(module.README, description, runner, module.ProblemConfig), nil
}

// Runner is a problem runner.
type Runner interface {
// Input generates the input for the problem.
Expand Down Expand Up @@ -84,7 +125,7 @@ func (p *CommandRunner) Part2Solution(ctx context.Context, seed int) (int64, err
}

func (p *CommandRunner) run(ctx context.Context, seed int, args string) (string, error) {
command := p.command + " " + args
command := fmt.Sprintf("%s --seed %d %s", p.command, seed, args)
logger := p.logger.With(
"seed", seed,
"command", command)
Expand Down Expand Up @@ -169,14 +210,13 @@ func (c *CachedRunner) Part2Solution(ctx context.Context, seed int) (int64, erro
return getCache(ctx, c, seed, part2CacheKey, c.runner.Part2Solution)
}

var errCacheMiss = errors.New("cache miss")

func getCache[T any](
ctx context.Context,
c *CachedRunner,
seed int, pkey problemCacheKey, fn func(context.Context, int) (T, error),
) (T, error) {
key := runnerCacheKey{c.problemID, seed, pkey}

logger := c.logger.With(
"seed", seed,
"key.id", key.id,
Expand Down
82 changes: 69 additions & 13 deletions server/problem/scoring.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,81 @@ func CalculateCooldownEnd(totalAttempts int, lastSubmitted, now time.Time) time.
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
)
// PointsPerPart is the number of points awarded for solving a part of a
// problem.
const PointsPerPart = 100

// 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
//
// Optional parameters:
//
// - If maxPoints is 0, it is set to PointsPerPart.
// - If version is 0, the latest scoring function is used.
func ScalePoints(t, startedAt time.Time, maxPoints float64, version ScoringVersion) float64 {
if maxPoints == 0 {
maxPoints = PointsPerPart
}
return version.fn()(t, startedAt) * maxPoints
}

// ScoringVersion is the version of the scoring function.
type ScoringVersion int

const (
_ ScoringVersion = iota
V1ScoreScaling
V2ScoreScaling

maxScoreScalingVersion // latest = maxScoreScalingVersion - 1
)

const latestScoreScalingVersion = maxScoreScalingVersion - 1

func (v ScoringVersion) IsValid() bool {
return 0 < v && v < maxScoreScalingVersion
}

func scoreScalingFn(x float64) float64 {
func (v ScoringVersion) fn() scoringFn {
switch v {
case 0:
return latestScoreScalingVersion.fn()
case V1ScoreScaling:
return scoreScalingV1
case V2ScoreScaling:
return scoreScalingV2
default:
panic("invalid scoring version")
}
}

type scoringFn func(t, startedAt time.Time) float64

var (
_ scoringFn = scoreScalingV1
_ scoringFn = scoreScalingV2
)

func scoreScalingV1(t, startedAt time.Time) float64 {
const maxHour = 24
// 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)) }
f1 := func(x float64) float64 { return (math.Atan(-math.Pi*x+math.Pi/2) / 4) + 0.75 }
f2 := func(x float64) float64 { return f1(x) + (1 - f1(0)) }
g := func(x float64) float64 { return clamp(f2(x), 0, 1) }
x := t.Sub(startedAt).Hours() / maxHour
return g(x)
}

func scoreScalingV2(t, startedAt time.Time) float64 {
// https://www.desmos.com/calculator/adpqv3xqzr
const maxHour = 12
const intensity = 6.7
const phase = 1.1
const m = 3.4
f1 := func(x float64) float64 { return math.Atan(-m*x+phase) / intensity }
f2 := func(x float64) float64 { return f1(x) + (1 - f1(0)) }
g := func(x float64) float64 { return clamp(f2(x), 0, 1) }
x := t.Sub(startedAt).Hours() / maxHour
return g(x)
}

Expand Down
Loading

0 comments on commit e748071

Please sign in to comment.