Skip to content

Commit 8098871

Browse files
committed
Reworks balloon for sending balloons in freeze
Balloons can be quite a motivator for a team and it can be frustrating to spend >4 hours solving a problem to only get an AC during the freeze. We used to have the show_balloons_postfreeze configflag to send balloons during the freeze but this would always send balloons during the freeze. show_balloons_postfreeze has now been replaced with 'minimum_number_of_balloons'. This setting now expresses the minimum number of balloons to send to a team even during the freeze. To prevent an information leak only balloons for problems that have been solved before the freeze can be sent out. Leaving this value to 0 keeps the 'old' behavior of not sending any balloons while setting it to a value >#contestproblems results in the old behaviour of sending all balloons during the freeze.
1 parent 2357c0c commit 8098871

File tree

2 files changed

+73
-52
lines changed

2 files changed

+73
-52
lines changed

etc/db-config.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -255,11 +255,11 @@
255255
default_value: false
256256
public: true
257257
description: Show results of TOO-LATE submissions in team interface?
258-
- name: show_balloons_postfreeze
259-
type: bool
260-
default_value: false
258+
- name: minimum_number_of_balloons
259+
type: int
260+
default_value: 0
261261
public: true
262-
description: Give out balloon notifications after the scoreboard has been frozen?
262+
description: How many balloons to hand out ignoring freeze time. Only hands out balloons for problems solved pre-freeze.
263263
- name: show_relative_time
264264
type: bool
265265
default_value: false

webapp/src/Service/BalloonService.php

Lines changed: 69 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,13 @@ public function updateBalloons(
9292
public function collectBalloonTable(Contest $contest, bool $todo = false): array
9393
{
9494
$em = $this->em;
95-
$showPostFreeze = (bool)$this->config->get('show_balloons_postfreeze');
96-
if (!$showPostFreeze) {
97-
$freezetime = $contest->getFreezeTime();
98-
}
9995

96+
// Retrieve all relevant balloons in 'submit order'. This allows accurate
97+
// counts when deciding whether to hand out post-freeze balloons.
10098
$query = $em->createQueryBuilder()
10199
->select('b', 's.submittime', 'p.probid',
102100
't.teamid', 's', 't', 't.location',
103-
'c.categoryid AS categoryid', 'c.name AS catname',
101+
'c.categoryid AS categoryid', 'c.sortorder', 'c.name AS catname',
104102
'co.cid', 'co.shortname',
105103
'cp.shortname AS probshortname', 'cp.color',
106104
'a.affilid AS affilid', 'a.shortname AS affilshort')
@@ -114,28 +112,20 @@ public function collectBalloonTable(Contest $contest, bool $todo = false): array
114112
->leftJoin('t.affiliation', 'a')
115113
->andWhere('co.cid = :cid')
116114
->setParameter('cid', $contest->getCid())
117-
->orderBy('b.done', 'ASC')
118-
->addOrderBy('s.submittime', 'DESC');
115+
->orderBy('b.done', 'DESC')
116+
->addOrderBy('s.submittime', 'ASC');
119117

120118
$balloons = $query->getQuery()->getResult();
121-
// Loop once over the results to get totals.
122-
$TOTAL_BALLOONS = [];
123-
foreach ($balloons as $balloonsData) {
124-
if ($balloonsData['color'] === null) {
125-
continue;
126-
}
127119

128-
$stime = $balloonsData['submittime'];
120+
$minumumNumberOfBalloons = (int)$this->config->get('minimum_number_of_balloons');
121+
$freezetime = $contest->getFreezeTime();
129122

130-
if (isset($freezetime) && $stime >= $freezetime) {
131-
continue;
132-
}
123+
$balloonsTable = [];
133124

134-
$TOTAL_BALLOONS[$balloonsData['teamid']][$balloonsData['probshortname']] = $balloonsData[0]->getSubmission()->getContestProblem();
135-
}
125+
// Total balloons keeps track of the total balloons for a team, will be used to fill the rhs for every row in $balloonsTable.
126+
// The same summary is used for every row for a team. References to elements in this array ensure easy updates.
127+
$balloonSummaryPerTeam = [];
136128

137-
// Loop again to construct table.
138-
$balloons_table = [];
139129
foreach ($balloons as $balloonsData) {
140130
if ($balloonsData['color'] === null) {
141131
continue;
@@ -144,41 +134,72 @@ public function collectBalloonTable(Contest $contest, bool $todo = false): array
144134
$balloon = $balloonsData[0];
145135
$done = $balloon->getDone();
146136

147-
if ($todo && $done) {
148-
continue;
137+
// Ensure a summary-row exists for this sortorder and take a reference to these summaries. References are needed to ensure array reuse.
138+
// Summaries are used to determine whether a balloon has been handed out so they need to be separated between sortorders.
139+
$balloonSummaryPerTeam[$balloonsData['sortorder']] ??= [];
140+
$relevantBalloonSummaries = &$balloonSummaryPerTeam[$balloonsData['sortorder']];
141+
142+
// Commonly no balloons are handed out post freeze.
143+
// Underperforming teams' moral can be boosted by handing out balloons post-freeze.
144+
// Handing out balloons for problems that have not been solved pre-freeze poses a potential information leak, so these are always excluded.
145+
// So to decide whether to skip showing a balloon:
146+
// 1. Check whether the scoreboard has been frozen.
147+
// 2. Check whether the team has exceeded minimum number of balloons.
148+
// 3. Check whether the problem been solved pre-freeze.
149+
$stime = $balloonsData['submittime'];
150+
if (isset($freezetime) && $stime >= $freezetime) {
151+
if (key_exists($balloonsData['teamid'], $relevantBalloonSummaries) &&
152+
count($relevantBalloonSummaries[$balloonsData['teamid']]) >= $minumumNumberOfBalloons) {
153+
continue;
154+
}
155+
156+
// Check if problem has been solved before the freeze by someone in the same sortorder to prevent information leak.
157+
// The DOMjudge team (that commonly runs jury submissions) has so must be ignored.
158+
// If a balloon for this problem should've been handed out it is safe to hand out again since balloons are handled in 'submit order'.
159+
if (!array_reduce($relevantBalloonSummaries, fn($c, $i) => $c ||
160+
array_key_exists($balloonsData['probshortname'], $i), false)) {
161+
continue;
162+
}
149163
}
150164

151-
$balloonId = $balloon->getBalloonId();
165+
// Register the balloon that is handed out in the team summary.
166+
$relevantBalloonSummaries[$balloonsData['teamid']][$balloonsData['probshortname']] = $balloon->getSubmission()->getContestProblem();
152167

153-
$stime = $balloonsData['submittime'];
154-
155-
if (isset($freezetime) && $stime >= $freezetime) {
168+
// This balloon might not need to be listed, entire order is needed for counts though.
169+
if ($todo && $done) {
156170
continue;
157171
}
158172

159-
$balloondata = [];
160-
$balloondata['balloonid'] = $balloonId;
161-
$balloondata['time'] = $stime;
162-
$balloondata['problem'] = $balloonsData['probshortname'];
163-
$balloondata['contestproblem'] = $balloon->getSubmission()->getContestProblem();
164-
$balloondata['team'] = $balloon->getSubmission()->getTeam();
165-
$balloondata['teamid'] = $balloonsData['teamid'];
166-
$balloondata['location'] = $balloonsData['location'];
167-
$balloondata['affiliation'] = $balloonsData['affilshort'];
168-
$balloondata['affiliationid'] = $balloonsData['affilid'];
169-
$balloondata['category'] = $balloonsData['catname'];
170-
$balloondata['categoryid'] = $balloonsData['categoryid'];
171-
172-
ksort($TOTAL_BALLOONS[$balloonsData['teamid']]);
173-
$balloondata['total'] = $TOTAL_BALLOONS[$balloonsData['teamid']];
174-
175-
$balloondata['done'] = $done;
176-
177-
$balloons_table[] = [
178-
'data' => $balloondata,
173+
$balloonsTable[] = [
174+
'data' => [
175+
'balloonid' => $balloon->getBalloonId(),
176+
'time' => $stime,
177+
'problem' => $balloonsData['probshortname'],
178+
'contestproblem' => $balloon->getSubmission()->getContestProblem(),
179+
'team' => $balloon->getSubmission()->getTeam(),
180+
'teamid' => $balloonsData['teamid'],
181+
'location' => $balloonsData['location'],
182+
'affiliation' => $balloonsData['affilshort'],
183+
'affiliationid' => $balloonsData['affilid'],
184+
'category' => $balloonsData['catname'],
185+
'categoryid' => $balloonsData['categoryid'],
186+
'done' => $done,
187+
188+
// Reuse the same total summary table by taking a reference, makes updates easier.
189+
'total' => &$relevantBalloonSummaries[$balloonsData['teamid']],
190+
]
179191
];
180192
}
181-
return $balloons_table;
193+
194+
// Sort the balloons, since these are handled by reference each summary item only need to be sorted once.
195+
foreach ($balloonSummaryPerTeam as $relevantBalloonSummaries) {
196+
foreach ($relevantBalloonSummaries as &$balloons) {
197+
ksort($balloons);
198+
}
199+
}
200+
201+
// Reverse the order so the newest appear first
202+
return array_reverse($balloonsTable);
182203
}
183204

184205
public function setDone(int $balloonId): void

0 commit comments

Comments
 (0)