['admin.competition.delete_problem': "/admin/competitions/delete_problem"]: controller "admin.competition.delete_problem"
['admin.competition.activate': "/admin/competitions/activate/:competition_id"]: controller "admin.competition.activate"
+ ['admin.utils.score': "/admin/score"]: controller "admin.utils.score"
+
"/console": console.make!
make_controller
inject:
crypto: 'crypto'
+ scoring: 'scoring'
middleware: { 'logged_out' }
table.insert @errors, 'Error creating account'
return render: 'account.register'
+ @scoring\rescore_everything!
+
@session.user_id = user_id
redirect_to: @url_for 'index'
import Competitions, Problems, CompetitionProblems from require 'models'
make_controller
+ inject:
+ scoring: 'scoring'
+
middleware: { 'logged_in', 'admin_required' }
post: capture_errors_json =>
if #comp_prob > 0
yield_error 'Problem already in competition'
return
-
+
CompetitionProblems\create {
competition_id: @params.competition_id
problem_id: problem.id
letter: @params.letter
}
+ @scoring\rescore_everything!
+
redirect_to: @url_for 'admin.competition.edit', { competition_id: @params.competition_id }
import make_controller from require "controllers.controller"
import assert_valid from require "lapis.validate"
import capture_errors, capture_errors_json, yield_error from require 'lapis.application'
-import Competitions, Problems, CompetitionProblems from require 'models'
+import Jobs, CompetitionProblems from require 'models'
make_controller
+ inject:
+ scoring: 'scoring'
+
middleware: { 'logged_in', 'admin_required' }
get: capture_errors_json =>
for cp in *comp_prob
cp\delete!
- redirect_to: @url_for "admin.competition.edit", { competition_id: @params.competition_id }
\ No newline at end of file
+ jobs = Jobs\select "where competition_id=? and problem_id=?", @params.competition_id, @params.problem_id
+ for j in *jobs
+ j\delete!
+
+ @scoring\rescore_everything!
+
+ redirect_to: @url_for "admin.competition.edit", { competition_id: @params.competition_id }
{ "name", exists: true }
{ "start_time", exists: true }
{ "end_time", exists: true }
+ { "time_offset", exists: true }
+ { "programming_points", exists: true }
+ { "codegolf_points", exists: true }
+ { "word_points", exists: true }
}
@comp = Competitions\find @params.id
name: @params.name
start: @params.start_time
end: @params.end_time
+ time_offset: @params.time_offset
+ programming_points: @params.programming_points
+ codegolf_points: @params.codegolf_points
+ word_points: @params.word_points
}
@comp\refresh!
{ "short_name", exists: true }
{ "description", exists: true }
{ "time_limit", exists: true, is_integer: true }
+ { "kind", exists: true }
}
- problem = Problems\find @params.problem_id
- unless problem
+ @problem = Problems\find @params.problem_id
+ unless @problem
yield_error "Problem with id '#{@params.problem_id}' not found"
return
- problem\update {
+ @problem\update {
name: @params.name
short_name: @params.short_name
description: @params.description
time_limit: @params.time_limit
+ kind: Problems.kinds\for_db @params.kind
}
redirect_to: (@url_for "admin.problem.edit", problem_name: @params.short_name)
--- /dev/null
+include_rules
--- /dev/null
+import make_controller from require "controllers.controller"
+
+make_controller
+ inject:
+ scoring: 'scoring'
+
+ middleware: { 'logged_in', 'admin_required' }
+
+ get: =>
+ @scoring\setup_scoring_tables!
+ @scoring\score_all!
+ @scoring\place!
+
+ return json: 'Scored all users'
}
if status.status != Jobs.statuses.running
- problem = Problems\find job.problem_id
- @scoring\score_problem_for_user job.user_id, problem.short_name
+ @scoring\score job.user_id, job.problem_id
@scoring\place!
json: { status: 'success' }
import Competitions from require 'models'
-{:time_to_number} = (require 'utils.time')!
-
=>
@competition = Competitions\find active: true
unless @competition
@write json: 'No active competition'
-
+
current_time = os.time()
- start_time = time_to_number @competition.start
+ start_time = @competition\get_start_time_num!
unless start_time <= current_time
- @write render: 'competition.not_started'
\ No newline at end of file
+ @write render: 'competition.not_started'
import Competitions from require 'models'
-{:time_to_number} = (require 'utils.time')!
-
=>
@competition = Competitions\find active: true
unless @competition
@write json: 'No active competition'
-
+
current_time = os.time()
- start_time = time_to_number @competition.start
- end_time = time_to_number @competition.end
+ start_time = @competition\get_start_time_num!
+ end_time = @competition\get_end_time_num!
unless start_time <= current_time
@write render: 'competition.not_started'
unless current_time <= end_time
- @write render: 'competition.finished'
\ No newline at end of file
+ @write render: 'competition.finished'
{ "attempts", types.integer default: 0 }
{ "status", types.enum default: 1 }
}
+
+ [10]: =>
+ add_column "competitions", "programming_points", (types.integer default: 1000)
+ add_column "competitions", "codegolf_points", (types.integer default: 750)
+ add_column "competitions", "word_points", (types.integer default: 500)
+
+ add_column "competitions", "time_offset", (types.integer default: 0)
}
import Model from require 'lapis.db.model'
db = require 'lapis.db'
+time = (require 'utils.time')!
class Competitions extends Model
@relations: {
{ "leaderboard", fetch: =>
db.select "* from leaderboard_placements where competition_id=? order by place asc", @id
}
- }
+ { "start_time_num", fetch: =>
+ (time.time_to_number @start) + @time_offset * 60
+ }
+ { "end_time_num", fetch: =>
+ (time.time_to_number @['end']) + @time_offset * 60
+ }
+ }
return 0 if #res == 0
res[1].sum
+get_codegolf_leaders = (problem_id, competition_id) ->
+ db.select "user_id, problem_id, competition_id, status, time_initiated, char_length(code) as bytes
+ from jobs
+ where problem_id=? and competition_id=?
+ order by status asc, bytes asc, time_initiated asc", problem_id, competition_id
+
+clear_codegolf_scores = (problem_id, competition_id) ->
+ db.query "update leaderboard_problems
+ set points=0, status=1
+ from leaderboard_placements
+ where problem_id=? and leaderboard_placements.competition_id=?", problem_id, competition_id
-> {
:has_correct_submission
:get_first_correct_submission
:delete_leaderboard_for_competition
:get_user_score
+ :get_codegolf_leaders
+ :clear_codegolf_scores
}
import Injectable from require 'utils.inject'
-import Users, Problems, Competitions, LeaderboardProblems, LeaderboardPlacements from require 'models'
+import Users, Jobs, Problems, Competitions, LeaderboardProblems, LeaderboardPlacements from require 'models'
+require 'utils.table'
class Scoring extends Injectable
new: =>
@queries = @make 'queries'
@time = @make 'time'
+
+ -- Get the currently active competition
@competition = Competitions\find active: true
- @competition_start = @time.time_to_number @competition.start
- @competition_end = @time.time_to_number @competition.end
+ -- Load the problems and users that are going to be scored
+ @comp_problems = @competition\get_competition_problems!
+ @users = Users\select!
+
+ -- Get the start and end time in a UTC time number
+ @competition_start = @competition\get_start_time_num!
+ @competition_end = @competition\get_end_time_num!
setup_scoring_tables: =>
+ -- Delete all old leaderboard entries for this competition
@queries.delete_leaderboard_for_competition @competition.id
- users = Users\select!
- problems = @competition\get_competition_problems!
+ -- Refetch the problems and users in case they have changed
+ @comp_problems = @competition\get_competition_problems!
+ @users = Users\select!
- for user in *users
+ for user in *@users
+ -- Create a placement on the leaderboard for each user
placement = LeaderboardPlacements\create
competition_id: @competition.id
user_id: user.id
- for problem in *problems
+ for problem in *@comp_problems
+ -- Create a problem on the placement for each problem
LeaderboardProblems\create
leaderboard_placement_id: placement.id
user_id: user.id
problem_id: problem.problem_id
get_problem_worth: (time_submitted) =>
+ -- Start the linear decent of the problem worth
+ -- 30 minutes after the competition starts so
+ -- they have a chance to submit a problem
+ -- or two before they lose points
+
start = @competition_start + 30 * 60
return 1 if time_submitted < start
+
duration = @competition_end - start
percent = (time_submitted - start) / duration
1 - 0.5 * percent
- score_problem_for_user: (user_id, problem_shortname) =>
+ score_problem_for_user: (user_id, problem) =>
+ -- Get the placement for the user during this competition
placement = LeaderboardPlacements\find user_id: user_id, competition_id: @competition.id
+
status = LeaderboardProblems.statuses.not_attempted
points = 0
attempts = 0
- problem = Problems\find short_name: problem_shortname
-
- -- THIS SHOULD SWITCH ON PROBLEM KIND
-
- attempts += @queries.count_incorrect_submission user_id, problem_shortname
+ -- Count the incorrect submissions, and if there are
+ -- any, set the status of the problem to be wrong
+ attempts += @queries.count_incorrect_submission user_id, problem.short_name
if attempts > 0
status = LeaderboardProblems.statuses.wrong
- if @queries.has_correct_submission user_id, problem_shortname
+ -- If there is a correct submission, get the first one
+ -- (best for problem worth) and compute the points
+ -- for this problem:
+ -- points = programming_points * worth - 50 * wrong_attempts
+ if @queries.has_correct_submission user_id, problem.short_name
job = @queries.get_first_correct_submission user_id, problem.id, @competition.id
- points += math.ceil (1000 * @get_problem_worth job.time_initiated)
+ points += math.ceil (@competition.programming_points * @get_problem_worth job.time_initiated)
points -= 50 * attempts
status = LeaderboardProblems.statuses.correct
attempts += 1
+ -- Update the problem in the database
lp = LeaderboardProblems\find problem_id: problem.id, leaderboard_placement_id: placement.id
lp\update
status: status
points: points
attempts: attempts
+ score_codegolf: (problem) =>
+ -- Clear the codegolf scores for this problem in this competition
+ @queries.clear_codegolf_scores problem.id, @competition.id
+
+ -- Get the best code golf submisssions
+ leaders = @queries.get_codegolf_leaders problem.id, @competition.id
+
+ points = @competition.codegolf_points
+ third_points = points / 3
+ placed_leaders = {}
+
+ -- For each submission, if the user has already attained
+ -- points, skip that submission
+ for leader in *leaders
+ continue if table.contains placed_leaders, leader.user_id
+ table.insert placed_leaders, leader.user_id
+
+ placement = LeaderboardPlacements\find user_id: leader.user_id, competition_id: @competition.id
+ lp = LeaderboardProblems\find problem_id: problem.id, leaderboard_placement_id: placement.id
+ if leader.status == Jobs.statuses.correct
+ lp\update
+ status: LeaderboardProblems.statuses.correct
+ points: points
+ attempts: leader.bytes
+ points -= third_points if points > 0
+ else
+ lp\update
+ status: LeaderboardProblems.statuses.wrong
+ points: 0
+ attempts: 0
+
score_user: (user_id) =>
- problems = @competition\get_competition_problems!
- for p in *problems
+ for p in *@comp_problems
problem = p\get_problem!
- @score_problem_for_user user_id, problem.short_name
+ @score_problem_for_user user_id, problem
score_problem: (problem_name) =>
- users = Users\select!
- for u in *users
- @score_problem_for_user u.id, problem_name
+ problem = Problems\find short_name: problem_name
+ for u in *@users
+ @score_problem_for_user u.id, problem
score_all: =>
- problems = @competition\get_competition_problems!
- for p in *problems
- problem = p\get_problem!
- @score_problem problem.short_name
+ for p in *@comp_problems
+ for u in *@users
+ @score u.id, p.problem_id
- place: =>
- users = Users\select!
+ score: (user_id, problem_id) =>
+ problem = Problems\find problem_id
+ switch problem.kind
+ when Problems.kinds.code then @score_problem_for_user user_id, problem
+ when Problems.kinds.golf then @score_codegolf problem
+ when Problems.kinds.word then @score_word_for_user user_id, problem
- for u in *users
+ place: =>
+ for u in *@users
u.score = @queries.get_user_score u.id, @competition.id
- table.sort users, (a, b) ->
- a.score - b.score
+ table.sort @users, (a, b) ->
+ a.score > b.score
+ -- If n users have the same score, they are both place x, and
+ -- the next person is place x + n.
last_score = 1e308
num, act_num = 0, 0
- for u in *users
+ for u in *@users
act_num += 1
if last_score > u.score
num = act_num
last_score = u.score
+
lp = LeaderboardPlacements\find user_id: u.id, competition_id: @competition.id
lp\update
place: num
score: u.score
+
+ rescore_everything: =>
+ -- Completely resets everything if a problem is
+ -- added or removed or if a user registers
+ @setup_scoring_tables!
+ @score_all!
+ @place!
--- /dev/null
+table.contains = (t, v) ->
+ for a in *t
+ return true if a == v
+ return false
h1 "Editing '#{@comp.name}'"
div class: 'content', ->
- div class: 'split-2', ->
+ div class: 'split-2-1', ->
form method: 'POST', ->
input type: 'hidden', name: 'id', value: "#{@comp.id}", ""
- label for: 'name', 'Competition name'
- input type: 'text', name: 'name', value: "#{@comp.name}", ""
+ div class: 'split-3-1', ->
+ div class: 'mar-r-24', ->
+ label for: 'name', 'Competition name'
+ input type: 'text', name: 'name', value: "#{@comp.name}", ""
+ div ->
+ label for: 'time_offset', 'Time offset (in minutes)'
+ input type: 'number', name: 'time_offset', value: "#{@comp.time_offset}", ""
- label for: 'start_time', 'Start time'
- input type: 'datetime-local', name: 'start_time', value: "#{@comp.start}", ""
+ div class: 'split-2', ->
+ div ->
+ label for: 'start_time', 'Start time'
+ input type: 'datetime-local', name: 'start_time', value: "#{@comp.start}", ""
- label for: 'end_time', 'End time'
- input type: 'datetime-local', name: 'end_time', value: "#{@comp.end}", ""
+ div ->
+ label for: 'end_time', 'End time'
+ input type: 'datetime-local', name: 'end_time', value: "#{@comp.end}", ""
- input type: 'submit', value: 'Save competition'
-
- div ->
+ div class: 'mar-t-48', -> text ""
+
+ div class: 'split-3', ->
+ div class: 'mar-r-12', ->
+ label for: 'programming_points', 'Programming points'
+ input type: 'number', name: 'programming_points', value: "#{@comp.programming_points}", ""
+
+ div class: 'mar-r-12 mar-l-12', ->
+ label for: 'codegolf_points', 'Code golf points'
+ input type: 'number', name: 'codegolf_points', value: "#{@comp.codegolf_points}", ""
+
+ div class: 'mar-l-12', ->
+ label for: 'word_points', 'Word points'
+ input type: 'number', name: 'word_points', value: "#{@comp.word_points}", ""
+
+ input class: 'mar-t-24', type: 'submit', value: 'Save competition'
+
+ div class: 'mar-l-24', ->
div class: 'header-line', ->
span "Problems"
html = require "lapis.html"
TestCase = require "views.admin.problem.test_case"
+import Problems from require 'models'
class AdminProblemEdit extends html.Widget
content: =>
label for: 'name', 'Problem name'
input type: 'text', name: 'name', placeholder: 'Problem name', value: @problem.name, ""
- div class: 'split-2', ->
- div ->
+ div class: 'split-3', ->
+ div class: 'mar-r-12', ->
label for: 'name', 'Short name'
input type: 'text', name: 'short_name', placeholder: 'Short URL name', value: @problem.short_name, ""
- div ->
+ div class: 'mar-l-12 mar-r-12', ->
label for: 'name', 'Time limit'
input type: 'number', value: 500, name: 'time_limit', value: @problem.time_limit, ""
+ div class: 'mar-l-12', ->
+ label for: 'kind', 'Problem kind'
+ element 'select', name: 'kind', ->
+ option { value: 'code', selected: @problem.kind == Problems.kinds.code }, 'Programming'
+ option { value: 'golf', selected: @problem.kind == Problems.kinds.golf }, 'Code Golf'
+ option { value: 'word', selected: @problem.kind == Problems.kinds.word }, 'Word'
+
div class: 'header-line', -> div 'Problem description'
pre { style: 'height: 32rem;', id: 'code-editor', 'data-lang': 'markdown' }, @problem.description
input class: 'mar-t-24', type: 'submit', value: 'Update problem info'
div class: 'mar-t-24', ->
- h2 class: 'pad-l-12', 'Test cases'
+ h2 class: 'pad-l-12', 'Test cases'
div class: 'test-cases', ->
for test_case in *@test_cases
button { 'data-new-tc': @problem.short_name }, 'New test case'
button { 'data-tc-save-all': true }, 'Save all'
-
\ No newline at end of file