Skip to main content

Overview

The scoreboard displays real-time rankings for assignments, showing student scores, solve counts, and detailed statistics.

Scoreboard Model

Scoreboard.php:13
protected $fillable = ['assignment_id', 'scoreboard'];
Each assignment has one scoreboard that stores pre-rendered HTML:
Scoreboard.php:15-17
public function assignment(){
    return $this->belongsTo('App\Models\Assignment');
}

Scoreboard Modes

Wecode provides three scoreboard views:

Full

Route: /scoreboard/full/{id}Complete scoreboard with all statistics, problem-wise breakdown, and submission details.

Plain

Route: /scoreboard/plain/{id}Simplified text-only view without detailed statistics, suitable for display.

Simplify

Route: /scoreboard/simplify/{id}Minimal scoreboard showing only essential information.

Generating Scoreboards

Core Generation Logic

Scoreboard.php:19-178
private function _generate_scoreboard()
{
    CarbonInterval::setCascadeFactors([
        'minute' => [60, 'seconds'], 
        'hour' => [60, 'minutes']
    ]);

    $assignment = $this->assignment;
    $submissions = $assignment->submissions->where('is_final',1);
    
    $total_score = array();
    $total_accepted_score = array();
    $solved = array();
    $tried_to_solve = array();
    $penalty = array();
    $users = array();
    $scores = array();

    $problems = $assignment->problems->keyBy('id');
    $number_of_submissions = [];
    
    // Count submissions per user per problem
    foreach($assignment->submissions as $item)
    {
        $number_of_submissions[$item->user->username][$item->problem_id]=0;
    }
    foreach($assignment->submissions as $item)
    {
        $number_of_submissions[$item->user->username][$item->problem_id]+=1;
    }

    // Process each final submission
    foreach($submissions as $submission)
    {
        $pre_score = ceil(
            $submission->pre_score *
            ($problems[$submission->problem_id]->pivot->score ?? 0) / 10000
        );
        
        if ($submission['coefficient'] === 'error') 
            $final_score = 0;
        else 
            $final_score = ceil($pre_score * $submission['coefficient'] / 100);

        $fullmark = ($submission->pre_score == 10000);
        $time = CarbonInterval::seconds(
            $assignment->start_time->diffInSeconds($submission->created_at, true)
        )->cascade();
        $late = CarbonInterval::seconds(
            $assignment->finish_time->diffInSeconds($submission->created_at)
        )->cascade();
        
        $username = $submission->user->username;
        $scores[$username][$submission->problem_id]['score'] = $final_score;
        $scores[$username][$submission->problem_id]['time'] = $time;
        $scores[$username][$submission->problem_id]['late'] = $late;
        $scores[$username][$submission->problem_id]['fullmark'] = $fullmark;
        $scores[$username]['id'] = $submission->user_id;
        
        // Initialize counters
        if (!isset($total_score[$username])){
            $total_score[$username] = 0;
            $total_accepted_score[$username] = 0;
        }
        if (!isset($solved[$username])){
            $solved[$username] = 0;
            $tried_to_solve[$username] = 0;
        }
        if (!isset($penalty[$username]))
            $penalty[$username] = CarbonInterval::seconds(0);

        $solved[$username] += $fullmark;
        $tried_to_solve[$username] += 1;
        $total_score[$username] += $final_score;
        if ($fullmark) 
            $total_accepted_score[$username] += $final_score;

        // Calculate penalty time
        if($fullmark && $final_score > 0) {
            $penalty[$username]->add(
                $time->totalSeconds +
                ($number_of_submissions[$submission->user->username][$submission->problem_id]-1)
                * Setting::get('submit_penalty'), 
                'seconds'
            );
        }
        $users[] = $submission->user;
    }

    // Build scoreboard array
    $scoreboard = array(
        'username' => array(),
        'user_id' => array(),
        'score' => array(),
        'lops' => $lopsnames,
        'accepted_score' => array(),
        'submit_penalty' => array(),
        'solved' => array(),
        'tried_to_solve' => array()
    );

    $users = array_unique($users);
    foreach($users as $user){
        array_push($scoreboard['username'], $user->username);
        array_push($scoreboard['score'], $total_score[$user->username]);
        array_push($scoreboard['accepted_score'], $total_accepted_score[$user->username]);
        array_push($scoreboard['submit_penalty'], $penalty[$user->username]);
        array_push($scoreboard['solved'], $solved[$user->username]);
        array_push($scoreboard['tried_to_solve'], $tried_to_solve[$user->username]);
    }

    // Sort scoreboard
    array_multisort(
        $scoreboard['accepted_score'], SORT_NUMERIC, SORT_DESC,
        array_map(function($time){return $time->total('seconds');}, $scoreboard['submit_penalty']),
        $scoreboard['solved'], SORT_NUMERIC, SORT_DESC,
        $scoreboard['score'], SORT_NUMERIC, SORT_DESC,
        $scoreboard['username'],
        $scoreboard['tried_to_solve'],
        $scoreboard['submit_penalty'], SORT_NATURAL
    );

    return array($scores, $scoreboard, $number_of_submissions, $stat_print);
}

Ranking Algorithm

Scoreboard is sorted by (in order):
1

Accepted Score (Descending)

Total score from problems with 100% correct (pre_score = 10000)
2

Penalty Time (Ascending)

Submission time + (wrong attempts × penalty constant)
3

Problems Solved (Descending)

Number of problems with 100% correct
4

Total Score (Descending)

Sum of all final submission scores
5

Username (Alphabetical)

Tiebreaker by username
Scoreboard.php:125-134
array_multisort(
    $scoreboard['accepted_score'], SORT_NUMERIC, SORT_DESC,
    array_map(function($time){return $time->total('seconds');}, $scoreboard['submit_penalty']),
    $scoreboard['solved'], SORT_NUMERIC, SORT_DESC,
    $scoreboard['score'], SORT_NUMERIC, SORT_DESC,
    $scoreboard['username'],
    $scoreboard['tried_to_solve'],
    $scoreboard['submit_penalty'], SORT_NATURAL
);

Real-time Updates

Scoreboard updates automatically with rate limiting:
Scoreboard.php:234-248
public static function update_scoreboard($assignment_id)
{
    if ($assignment_id != 0) {
        $a = Scoreboard::firstOrCreate(
            ['assignment_id' => $assignment_id], 
            ['scoreboard' => ""]
        );

        // Rate limiter: only update once every 30 seconds
        if ($a->updated_at->lessthan(Carbon::now()->subSeconds(30))){
            $a->_update_scoreboard();
        }

        return true;
    }
}
Scoreboards update at most once per 30 seconds to prevent server overload.

Triggering Updates

Scoreboard updates when:
  • Final submission is selected
  • Submission is rejudged
  • Manually reloaded by instructor
submission_controller.php:393
Scoreboard::update_scoreboard($submission_curr->assignment_id);

Score Calculation

Per-Problem Scoring

Scoreboard.php:58-64
$pre_score = ceil(
    $submission->pre_score *
    ($problems[$submission->problem_id]->pivot->score ?? 0) / 10000
);

if ($submission['coefficient'] === 'error') 
    $final_score = 0;
else 
    $final_score = ceil($pre_score * $submission['coefficient'] / 100);

Penalty Time

ICPC-style penalty calculation:
Scoreboard.php:92-98
if($fullmark && $final_score > 0) {
    $penalty[$username]->add(
        $time->totalSeconds +  // Time from start to acceptance
        ($number_of_submissions[$submission->user->username][$submission->problem_id]-1)
        * Setting::get('submit_penalty'),  // Wrong attempt penalty
        'seconds'
    );
}

Problem Statistics

Scoreboard includes problem-wise statistics:
Scoreboard.php:136-173
$aggr = $assignment->submissions()
    ->groupBy('user_id', 'problem_id')
    ->select(DB::raw('user_id, problem_id, count(*) as submit'))
    ->get();
    
$aggr_ac = $assignment->submissions()
    ->groupBy('user_id', 'problem_id')
    ->where('pre_score', 10000)
    ->select(DB::raw('user_id, problem_id, count(*) as submit'))
    ->get();

foreach($problems as $id=>$p){
    $statistics[$id] ??= new class{};
    $a = & $statistics[$id];
    $a->tries = 0;
    $a->tries_user = 0;
    $a->solved = 0;
    $a->solved_user = 0;
}

foreach ($aggr as $ag ) {
    $statistics[$ag->problem_id] ??= new class{};
    $a = & $statistics[$ag->problem_id];
    $a->tries = ($a->tries ?? 0) + $ag->submit;
    $a->tries_user = ($a->tries_user ?? 0) + 1;
}

foreach ($aggr_ac as $ag ) {
    $a = & $statistics[$ag->problem_id];
    $a->solved = ($a->solved ?? 0) + $ag->submit;
    $a->solved_user = ($a->solved_user ?? 0) + 1;
}

$stat_print = array();
foreach($problems as $id=>$p){
    $a = &$statistics[$id];
    $stat_print[$id] = new class{};
    
    // "8 / 15 (53.33%)"
    $stat_print[$id]->solved_tries = "$a->solved / $a->tries " 
        . ($a->tries == 0 ? "" : "(" . round($a->solved * 100/$a->tries, 2) . "%)");
    
    // "5 / 10 (50%) (25% of all)"
    $stat_print[$id]->solved_tries_users = "$a->solved_user / $a->tries_user "
        . ($a->tries_user == 0 ? "" : "(" . round($a->solved_user * 100/$a->tries_user, 2) . "%)")
        . (count($users) == 0 ? "" : "(" . round($a->solved_user * 100/count($users), 2) . "%)");
    
    // Average attempts per user
    $stat_print[$id]->average_tries = ($a->tries == 0 ? "" : round($a->tries /$a->tries_user, 1));
    
    // Average attempts to solve
    $stat_print[$id]->average_tries_2_solve = ($a->solved == 0 ? "" : round($a->tries /$a->solved, 1));
}

Statistics Display

Solved/Tries

“8 / 15 (53.33%)”8 AC submissions out of 15 total submissions

Users

“5 / 10 (50%) (25%)”5 users solved out of 10 who tried (50% success rate, 25% of all participants)

Avg Attempts

“2.1”Average 2.1 submissions per user for this problem

Avg to Solve

“1.8”Average 1.8 attempts needed to solve

Scoreboard Rendering

HTML Generation

Scoreboard.php:180-232
public function _update_scoreboard()
{
    if ($this->assignment->id == 0)
        return false;

    $assignment = $this->assignment;

    if (!$assignment)
    {
        return false;
    }

    list ($scores, $scoreboard, $number_of_submissions,$stat_print) = $this->_generate_scoreboard();
    $all_problems = $assignment->problems;

    $total_score = 0;
    foreach($all_problems as $item)
        $total_score += $item->pivot->score;

    $all_name = User::all();
    foreach($all_name as $row)
    {
        $result[$row->username] = $row->display_name;
    }

    $data = array(
        'assignment_id' => $assignment->id,
        'problems' => $all_problems,
        'total_score' => $total_score,
        'scores' => $scores,
        'scoreboard' => $scoreboard,
        'names' => $result,
        'stat_print' => $stat_print,
        'no_of_problems'=> $assignment->problems->count(),
        'number_of_submissions' => $number_of_submissions,
    );

    $scoreboard_table = view('scoreboard_table', $data)->render();
    
    // Minify the scoreboard's HTML code
    $scoreboard_table = str_replace(["\n", "\r", "\t"], '', $scoreboard_table);
    $scoreboard_table = preg_replace('/ {2,}/', ' ', $scoreboard_table);

    $this->scoreboard = $scoreboard_table;
    $this->save();

    return true;
}

Class Scoreboards

Route: GET /lop/scoreboard/{lop} View combined scoreboard across all assignments in a class:
Scoreboard.php:42-46
$lopsnames = array();
foreach ($assignment->lops()->with('users')->get() as $key =>$lop) {
    foreach ($lop->users as $key => $user) {
        $lopsnames[$user->username] = $lop->name;
    }
}

Accessing Scoreboards

View Permissions

scoreboard_controller.php:33-52
public function index($assignment_id)
{
    $assignment = Assignment::find($assignment_id);
    
    if (in_array(Auth::user()->role->name, ['student']) && $assignment->score_board == false)
    {
        // Student can only view scoreboard if allowed
        abort(404, "This assignment does not have scoreboard");
    }
    
    $scoreboard = NULL;
    if ($assignment)
    {
        $scoreboard = $this->get_scoreboard($assignment_id);
    }
    
    return view('scoreboard', [
        'selected' => 'scoreboard',
        'place' => 'full',
        'assignment' => $assignment,
        'scoreboard' => $scoreboard,
    ]);
}

Get Scoreboard Data

scoreboard_controller.php:54-64
public function get_scoreboard($assignment_id)
{
    $query = DB::table('scoreboards')
        ->where('assignment_id',$assignment_id)
        ->get();

    if ($query->count() != 1)
        return false;
    else
    {
        return $query->first()->scoreboard;
    }
}

Simplified Views

Plain Mode

scoreboard_controller.php:95-106
public function plain($assignment_id){
    $assignment = Assignment::find($assignment_id);

    $data = array(
        'place' => 'plain',
        'assignment' => $assignment,
        'scoreboard' => strip_tags(
            $this->_strip_scoreboard($assignment_id), 
            "<table><thead><th><tbody><tr><td><br>"
        ),
        'selected' => 'scoreboard'
    );
    
    return view('scoreboard', $data);
}

Strip Scoreboard

scoreboard_controller.php:66-93
private function _strip_scoreboard($assignment_id){
    $a = $this->get_scoreboard($assignment_id);

    $dom = new DOMDocument;
    $dom->loadHTML('<?xml encoding="UTF-8">'. $a);
    
    // Remove paragraph tags
    $ps = $dom->getElementsByTagName('p');
    while($ps->length > 0){
        $ps[0]->parentNode->removeChild($ps[0]);
    }

    return $dom->saveXML($dom->getElementsByTagName('table')[0]);
}

Manual Reload

Route: GET /assignment/reload_scoreboard/{assignment_id} Instructors can force scoreboard regeneration:
assignment_controller.php:557-573
public function reload_scoreboard($assignment_id)
{
    if ( ! in_array( Auth::user()->role->name, ['admin', 'head_instructor', 'instructor']) )
        abort(403);
        
    $assignment = Assignment::find($assignment_id);
    if ($assignment == null){
        abort(404);
    }

    // Reset all final submission choices to the best score
    $assignment->reset_final_submission_choices();

    if (Scoreboard::update_scoreboard($assignment_id)){
        return redirect()->back()->with('success', 'Reload Scoreboard success');
    }
}

Scoreboard Routes

MethodRouteActionPermission
GET/scoreboard/full/{id}Full scoreboardparticipant (if enabled)
GET/scoreboard/plain/{id}Plain text viewparticipant (if enabled)
GET/scoreboard/simplify/{id}Simplified viewparticipant (if enabled)
GET/lop/scoreboard/{lop}Class scoreboardclass member
GET/assignment/reload_scoreboard/{id}Force reloadinstructor+

Best Practices

Configuration

  • Enable scoreboard for competitions
  • Disable for exams/assessments
  • Set appropriate penalty values
  • Test ranking with sample data

Performance

  • Cache pre-rendered HTML
  • Rate limit updates (30s minimum)
  • Monitor update frequency
  • Optimize for many participants

Display

  • Use plain mode for projectors
  • Refresh regularly during contests
  • Show statistics for analysis
  • Export for record keeping

Fairness

  • Consistent penalty calculation
  • Clear ranking criteria
  • Transparent score display
  • Fair tie-breaking rules
ICPC Mode: The default ranking prioritizes problems solved (AC submissions) over partial scores, with penalty time as the primary tiebreaker.

Build docs developers (and LLMs) love