Skip to main content

Overview

The Scoreboard model generates and caches HTML scoreboards for assignments. It calculates rankings, scores, penalties, and problem statistics based on final submissions.

Fillable Fields

assignment_id
integer
required
ID of the assignment this scoreboard belongs toUnique constraint: One scoreboard per assignment
scoreboard
text
Cached HTML content of the rendered scoreboard tableMinified to reduce storage size

Relationships

assignment()

assignment
BelongsTo<Assignment>
The assignment this scoreboard displays results for
$scoreboard->assignment // Returns Assignment model

Public Methods

update_scoreboard()

Static method to generate or update a scoreboard with rate limiting.
public static function update_scoreboard($assignment_id)
Parameters:
  • $assignment_id (integer) - Assignment ID to update
Returns: boolean - True if updated, false if skipped Rate limiting: Only updates once every 30 seconds per assignment Example:
// Called after new submission
Scoreboard::update_scoreboard($assignmentId);

// Skips if updated less than 30 seconds ago
Scoreboard::update_scoreboard($assignmentId);
Behavior:
  • Practice assignment (id=0): Returns false, no scoreboard created
  • First time: Creates new scoreboard record
  • Subsequent: Updates existing if > 30 seconds since last update

Private Methods

_update_scoreboard()

Generates scoreboard data and saves HTML.
private function _update_scoreboard()
Returns: boolean Process:
  1. Calls _generate_scoreboard() to calculate data
  2. Passes data to scoreboard_table view
  3. Minifies HTML output
  4. Saves to scoreboard field

_generate_scoreboard()

Calculates all scoreboard data from submissions.
private function _generate_scoreboard()
Returns: array containing:
  1. $scores - Detailed scores per user per problem
  2. $scoreboard - Sorted arrays for table display
  3. $number_of_submissions - Submission counts
  4. $stat_print - Problem statistics

Scoreboard Data Structure

User Scores Array

$scores = [
    'john_doe' => [
        'id' => 123,
        10 => [  // problem_id
            'score' => 80,
            'time' => CarbonInterval,  // Time from start
            'late' => CarbonInterval,  // Time after deadline (negative if early)
            'fullmark' => false,
        ],
        11 => [
            'score' => 100,
            'time' => CarbonInterval,
            'late' => CarbonInterval,
            'fullmark' => true,
        ],
    ],
    'jane_doe' => [...],
];

Scoreboard Rankings Array

$scoreboard = [
    'username' => ['john_doe', 'jane_doe', ...],
    'score' => [180, 150, ...],              // Total score
    'accepted_score' => [100, 100, ...],     // Score from fullmark problems only
    'submit_penalty' => [CarbonInterval, ...], // Penalty time
    'solved' => [1, 2, ...],                 // Number of fullmark problems
    'tried_to_solve' => [2, 3, ...],         // Number of attempted problems
    'lops' => [                              // Class names
        'john_doe' => 'CS101',
        'jane_doe' => 'CS102',
    ],
];

Problem Statistics

$stat_print = [
    10 => [  // problem_id
        'solved_tries' => '15 / 45 (33.33%)',
        'solved_tries_users' => '10 / 20 (50%) (66.67%)',
        'average_tries' => '2.3',
        'average_tries_2_solve' => '3.0',
    ],
];

Scoring Algorithm

1. Calculate Pre-Score

$pre_score = ceil(
    $submission->pre_score * 
    $problem->pivot->score / 10000
);

2. Apply Late Coefficient

if ($submission->coefficient === 'error') {
    $final_score = 0;
} else {
    $final_score = ceil($pre_score * $submission->coefficient / 100);
}

3. Determine Fullmark

$fullmark = ($submission->pre_score == 10000);

4. Calculate Time and Lateness

// Time from assignment start to submission
$time = CarbonInterval::seconds(
    $assignment->start_time->diffInSeconds($submission->created_at, true)
)->cascade();

// Time from deadline to submission (negative = early)
$late = CarbonInterval::seconds(
    $assignment->finish_time->diffInSeconds($submission->created_at)
)->cascade();

5. Calculate Penalty

Penalty only applies to fullmark submissions with score > 0:
if ($fullmark && $final_score > 0) {
    $penalty->add(
        $time->totalSeconds + 
        ($number_of_submissions - 1) * Setting::get('submit_penalty'),
        'seconds'
    );
}
Penalty formula:
penalty = submission_time + (failed_attempts * submit_penalty)

Ranking Sort Order

Scoreboards are sorted by:
  1. Accepted Score (descending) - Score from fullmark problems only
  2. Penalty Time (ascending) - Lower penalty ranks higher
  3. Solved Count (descending) - More fullmark problems ranks higher
  4. Total Score (descending) - Total points including partial credit
  5. Username (alphabetically)
array_multisort(
    $scoreboard['accepted_score'], SORT_NUMERIC, SORT_DESC,
    $scoreboard['submit_penalty'], SORT_NATURAL, SORT_ASC,
    $scoreboard['solved'], SORT_NUMERIC, SORT_DESC,
    $scoreboard['score'], SORT_NUMERIC, SORT_DESC,
    $scoreboard['username']
);

Problem Statistics Calculation

Aggregation Queries

// Count all submissions per user-problem
$aggr = $assignment->submissions()
    ->groupBy('user_id', 'problem_id')
    ->select(DB::raw('user_id, problem_id, count(*) as submit'))
    ->get();

// Count accepted submissions per user-problem
$aggr_ac = $assignment->submissions()
    ->groupBy('user_id', 'problem_id')
    ->where('pre_score', 10000)
    ->select(DB::raw('user_id, problem_id, count(*) as submit'))
    ->get();

Statistics Structure

$statistics[$problem_id] = [
    'tries' => 45,              // Total submissions
    'tries_user' => 20,         // Users who attempted
    'solved' => 15,             // Accepted submissions
    'solved_user' => 10,        // Users who solved
];

Display Calculations

// "15 / 45 (33.33%)"
$solved_tries = "$solved / $tries (" . round($solved * 100 / $tries, 2) . "%)";

// "10 / 20 (50%) (66.67%)"
$solved_tries_users = "$solved_user / $tries_user (" 
    . round($solved_user * 100 / $tries_user, 2) . "%) ("
    . round($solved_user * 100 / count($users), 2) . "%)";

// Average attempts per user
$average_tries = round($tries / $tries_user, 1);

// Average attempts to solve
$average_tries_2_solve = round($tries / $solved, 1);

HTML Minification

The generated HTML is minified to reduce database storage:
// Remove newlines, returns, tabs
$scoreboard_table = str_replace(["\n", "\r", "\t"], '', $scoreboard_table);

// Collapse multiple spaces to single space
$scoreboard_table = preg_replace('/ {2,}/', ' ', $scoreboard_table);

Example Usage

// Update scoreboard after new submission
Scoreboard::update_scoreboard($assignmentId);

// Get scoreboard for display
$scoreboard = Scoreboard::where('assignment_id', $assignmentId)->first();
echo $scoreboard->scoreboard; // Outputs HTML table

// Get underlying assignment
$assignment = $scoreboard->assignment;
echo "Scoreboard for: {$assignment->name}";

// Force update (bypasses rate limit)
$scoreboard = Scoreboard::firstOrCreate(
    ['assignment_id' => $assignmentId],
    ['scoreboard' => '']
);
$scoreboard->_update_scoreboard();

Scoreboard Display Modes

While not enforced by the model, scoreboards can be displayed in different modes:

Full Scoreboard

Shows all participants with complete scores and statistics

Frozen Scoreboard

Stops updating at a certain time (typically 1 hour before contest end) - implemented at application level

User-Specific View

Students see only their own row - filtered at controller level

View Integration

The scoreboard uses the scoreboard_table Blade view:
$data = [
    'assignment_id' => $assignment->id,
    'problems' => $all_problems,           // Problem models
    'total_score' => $total_score,         // Sum of all problem scores
    'scores' => $scores,                   // User-problem scores
    'scoreboard' => $scoreboard,           // Sorted rankings
    'names' => $result,                    // Username => display_name
    'stat_print' => $stat_print,           // Problem statistics
    'no_of_problems' => $assignment->problems->count(),
    'number_of_submissions' => $number_of_submissions,
];

$html = view('scoreboard_table', $data)->render();

Performance Considerations

  1. Rate Limiting: 30-second minimum between updates prevents excessive recalculation
  2. Caching: HTML stored in database, served directly without recalculation
  3. Eager Loading: Uses with() to optimize queries:
    $assignment->lops()->with('users')->get()
    $assignment->submissions->where('is_final', 1)
    
  4. Indexed Queries: Queries use indexed columns (user_id, problem_id, is_final)

Special Cases

Practice Assignment (id=0)

No scoreboard is generated for the practice assignment:
if ($assignment_id != 0) {
    // Generate scoreboard
}

No Submissions

If assignment has no final submissions, generates empty scoreboard:
$scoreboard = [
    'username' => [],
    'score' => [],
    'accepted_score' => [],
    'submit_penalty' => [],
    'solved' => [],
    'tried_to_solve' => [],
    'lops' => [],
];

Multiple Classes

Users can belong to multiple lops, but scoreboard shows first match:
foreach ($assignment->lops()->with('users')->get() as $lop) {
    foreach ($lop->users as $user) {
        $lopsnames[$user->username] = $lop->name; // Last lop wins
    }
}

Build docs developers (and LLMs) love