Skip to content

Commit 90fa34f

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 ac920ac commit 90fa34f

File tree

2 files changed

+73
-50
lines changed

2 files changed

+73
-50
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 & 46 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,74 @@ 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+
// TODO explain what this var does.
138+
if (!key_exists($balloonsData['sortorder'], $balloonSummaryPerTeam)) {
139+
$balloonSummaryPerTeam[$balloonsData['sortorder']] = [];
149140
}
150141

151-
$balloonId = $balloon->getBalloonId();
142+
$relevantBalloonSummaries = &$balloonSummaryPerTeam[$balloonsData['sortorder']];
152143

144+
// Commonly no balloons are handed out post freeze.
145+
// Underperforming teams' moral can be boosted by handing out balloons post-freeze.
146+
// Handing out balloons for problems that have not been solved pre-freeze poses a potential information leak, so these are always excluded.
147+
// So to decide whether to skip showing a balloon:
148+
// 1. Check whether the scoreboard has been frozen.
149+
// 2. Check whether the team has exceeded minimum number of balloons.
150+
// 3. Check whether the problem been solved pre-freeze.
153151
$stime = $balloonsData['submittime'];
154-
155152
if (isset($freezetime) && $stime >= $freezetime) {
153+
if (key_exists($balloonsData['teamid'], $relevantBalloonSummaries) &&
154+
count($relevantBalloonSummaries[$balloonsData['teamid']]) >= $minumumNumberOfBalloons) {
155+
continue;
156+
}
157+
158+
// Check if problem has been solved before the freeze by someone in the same sortorder to prevent information leak.
159+
// The DOMjudge team (that commonly runs jury submissions) has so must be ignored.
160+
// 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'.
161+
if (!array_reduce($relevantBalloonSummaries, fn($c, $i) => $c ||
162+
array_key_exists($balloonsData['probshortname'], $i), false)) {
163+
continue;
164+
}
165+
}
166+
167+
// Register the balloon that is handed out in the team summary.
168+
$relevantBalloonSummaries[$balloonsData['teamid']][$balloonsData['probshortname']] = $balloon->getSubmission()->getContestProblem();
169+
170+
// This balloon might not need to be listed, entire order is needed for counts though.
171+
if ($todo && $done) {
156172
continue;
157173
}
158174

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,
175+
$balloonsTable[] = [
176+
'data' => [
177+
'balloonid' => $balloon->getBalloonId(),
178+
'time' => $stime,
179+
'problem' => $balloonsData['probshortname'],
180+
'contestproblem' => $balloon->getSubmission()->getContestProblem(),
181+
'team' => $balloon->getSubmission()->getTeam(),
182+
'teamid' => $balloonsData['teamid'],
183+
'location' => $balloonsData['location'],
184+
'affiliation' => $balloonsData['affilshort'],
185+
'affiliationid' => $balloonsData['affilid'],
186+
'category' => $balloonsData['catname'],
187+
'categoryid' => $balloonsData['categoryid'],
188+
'done' => $done,
189+
190+
// Reuse the same total summary table by taking a reference, makes updates easier.
191+
'total' => &$relevantBalloonSummaries[$balloonsData['teamid']],
192+
]
179193
];
180194
}
181-
return $balloons_table;
195+
196+
// Sort the balloons, since these are handled by reference each summary item only need to be sorted once.
197+
foreach ($balloonSummaryPerTeam as $relevantBalloonSummaries) {
198+
foreach ($relevantBalloonSummaries as &$balloons) {
199+
ksort($balloons);
200+
}
201+
}
202+
203+
// Reverse the order so the newest appear first
204+
return array_reverse($balloonsTable);
182205
}
183206

184207
public function setDone(int $balloonId): void

0 commit comments

Comments
 (0)