Overview
The scoreboard displays real-time rankings for assignments, showing student scores, solve counts, and detailed statistics.
Scoreboard Model
protected $fillable = [ 'assignment_id' , 'scoreboard' ];
Each assignment has one scoreboard that stores pre-rendered HTML:
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
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):
Accepted Score (Descending)
Total score from problems with 100% correct (pre_score = 10000)
Penalty Time (Ascending)
Submission time + (wrong attempts × penalty constant)
Problems Solved (Descending)
Number of problems with 100% correct
Total Score (Descending)
Sum of all final submission scores
Username (Alphabetical)
Tiebreaker by username
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:
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
$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:
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:
$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
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:
$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
Method Route Action Permission GET /scoreboard/full/{id}Full scoreboard participant (if enabled) GET /scoreboard/plain/{id}Plain text view participant (if enabled) GET /scoreboard/simplify/{id}Simplified view participant (if enabled) GET /lop/scoreboard/{lop}Class scoreboard class member GET /assignment/reload_scoreboard/{id}Force reload instructor+
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.