Skip to content

Commit 570510e

Browse files
committed
Allow selecting new problem types as part of the problem entity.
While you can select the new types, they won't function yet. Part of #2525 Problem types are defined here: https://icpc.io/problem-package-format/spec/2023-07-draft.html#type
1 parent 2f43d7b commit 570510e

12 files changed

+172
-61
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DoctrineMigrations;
6+
7+
use Doctrine\DBAL\Schema\Schema;
8+
use Doctrine\Migrations\AbstractMigration;
9+
10+
/**
11+
* Auto-generated Migration: Please modify to your needs!
12+
*/
13+
final class Version20250323190305 extends AbstractMigration
14+
{
15+
public function getDescription(): string
16+
{
17+
return '';
18+
}
19+
20+
public function up(Schema $schema): void
21+
{
22+
// this up() migration is auto-generated, please modify it to your needs
23+
$this->addSql('ALTER TABLE problem ADD types INT NOT NULL COMMENT \'Bitmask of problem types, default is pass-fail.\'');
24+
$this->addSql('UPDATE problem SET types = 1');
25+
$this->addSql('UPDATE problem SET types = 5 WHERE is_multipass_problem = 1');
26+
$this->addSql('UPDATE problem SET types = 9 WHERE combined_run_compare = 1');
27+
$this->addSql('UPDATE problem SET types = 13 WHERE combined_run_compare = 1 AND is_multipass_problem = 1');
28+
$this->addSql('ALTER TABLE problem DROP combined_run_compare, DROP is_multipass_problem');
29+
}
30+
31+
public function down(Schema $schema): void
32+
{
33+
// this down() migration is auto-generated, please modify it to your needs
34+
$this->addSql('ALTER TABLE problem ADD combined_run_compare TINYINT(1) DEFAULT 0 NOT NULL COMMENT \'Use the exit code of the run script to compute the verdict\', ADD is_multipass_problem TINYINT(1) DEFAULT 0 NOT NULL COMMENT \'Whether this problem is a multi-pass problem.\', DROP types');
35+
}
36+
37+
public function isTransactional(): bool
38+
{
39+
return false;
40+
}
41+
}

webapp/src/Controller/Jury/ContestController.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -714,7 +714,7 @@ public function prefetchAction(Request $request, int $contestId): Response
714714
$runConfig = Utils::jsonEncode(
715715
[
716716
'hash' => $runExec->getHash(),
717-
'combined_run_compare' => $problem->getCombinedRunCompare(),
717+
'combined_run_compare' => $problem->isInteractiveProblem(),
718718
]
719719
);
720720
$judgeTask = new JudgeTask();

webapp/src/Controller/Jury/ProblemController.php

+4-18
Original file line numberDiff line numberDiff line change
@@ -204,17 +204,10 @@ public function indexAction(): Response
204204
$problemdata['badges'] = ['value' => $badges];
205205

206206
// merge in the rest of the data
207-
$type = '';
208-
if ($p->getCombinedRunCompare()) {
209-
$type .= ' interactive';
210-
}
211-
if ($p->isMultipassProblem()) {
212-
$type .= ' multi-pass (max passes: ' . $p->getMultipassLimit() . ')';
213-
}
214207
$problemdata = array_merge($problemdata, [
215208
'num_contests' => ['value' => (int)($contestCounts[$p->getProbid()] ?? 0)],
216209
'num_testcases' => ['value' => (int)$row['testdatacount']],
217-
'type' => ['value' => $type],
210+
'type' => ['value' => $p->getTypesAsString()],
218211
]);
219212

220213
$data_to_add = [
@@ -304,7 +297,7 @@ public function exportAction(int $problemId): StreamedResponse
304297
$yaml = ['name' => $problem->getName()];
305298
if (!empty($problem->getCompareExecutable())) {
306299
$yaml['validation'] = 'custom';
307-
} elseif ($problem->getCombinedRunCompare() && !empty($problem->getRunExecutable())) {
300+
} elseif ($problem->isInteractiveProblem() && !empty($problem->getRunExecutable())) {
308301
$yaml['validation'] = 'custom interactive';
309302
}
310303

@@ -340,7 +333,7 @@ public function exportAction(int $problemId): StreamedResponse
340333
$compareExecutable = null;
341334
if ($problem->getCompareExecutable()) {
342335
$compareExecutable = $problem->getCompareExecutable();
343-
} elseif ($problem->getCombinedRunCompare()) {
336+
} elseif ($problem->isInteractiveProblem()) {
344337
$compareExecutable = $problem->getRunExecutable();
345338
}
346339
if ($compareExecutable) {
@@ -496,13 +489,6 @@ public function viewAction(Request $request, SubmissionService $submissionServic
496489
page: $request->query->getInt('page', 1),
497490
);
498491

499-
$type = '';
500-
if ($problem->getCombinedRunCompare()) {
501-
$type .= ' interactive';
502-
}
503-
if ($problem->isMultipassProblem()) {
504-
$type .= ' multi-pass (max passes: ' . $problem->getMultipassLimit() . ')';
505-
}
506492
$data = [
507493
'problem' => $problem,
508494
'problemAttachmentForm' => $problemAttachmentForm->createView(),
@@ -512,7 +498,7 @@ public function viewAction(Request $request, SubmissionService $submissionServic
512498
'defaultOutputLimit' => (int)$this->config->get('output_limit'),
513499
'defaultRunExecutable' => (string)$this->config->get('default_run'),
514500
'defaultCompareExecutable' => (string)$this->config->get('default_compare'),
515-
'type' => $type,
501+
'type' => $problem->getTypesAsString(),
516502
'showContest' => count($this->dj->getCurrentContests(honorCookie: true)) > 1,
517503
'showExternalResult' => $this->dj->shadowMode(),
518504
'lockedProblem' => $lockedProblem,

webapp/src/Controller/Jury/SubmissionController.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,7 @@ public function viewAction(
577577
'unjudgableReasons' => $unjudgableReasons,
578578
'verificationRequired' => (bool)$this->config->get('verification_required'),
579579
'claimWarning' => $claimWarning,
580-
'combinedRunCompare' => $submission->getProblem()->getCombinedRunCompare(),
580+
'combinedRunCompare' => $submission->getProblem()->isInteractiveProblem(),
581581
'requestedOutputCount' => $requestedOutputCount,
582582
'version_warnings' => [],
583583
'isMultiPassProblem' => $submission->getProblem()->isMultipassProblem(),

webapp/src/Entity/Problem.php

+82-20
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,6 @@ class Problem extends BaseApiEntity implements
8686
#[Serializer\Exclude]
8787
private ?string $special_compare_args = null;
8888

89-
#[ORM\Column(options: [
90-
'comment' => 'Use the exit code of the run script to compute the verdict',
91-
'default' => 0,
92-
])]
93-
#[Serializer\Exclude]
94-
private bool $combined_run_compare = false;
95-
9689
#[Assert\File]
9790
#[Serializer\Exclude]
9891
private ?UploadedFile $problemstatementFile = null;
@@ -108,12 +101,23 @@ class Problem extends BaseApiEntity implements
108101
#[Serializer\Exclude]
109102
private ?string $problemstatement_type = null;
110103

111-
#[ORM\Column(options: [
112-
'comment' => 'Whether this problem is a multi-pass problem.',
113-
'default' => 0,
114-
])]
104+
public const TYPE_PASS_FAIL = 1;
105+
public const TYPE_SCORING = 2;
106+
public const TYPE_MULTI_PASS = 4;
107+
public const TYPE_INTERACTIVE = 8;
108+
public const TYPE_SUBMIT_ANSWER = 16;
109+
110+
private array $typesToString = [
111+
self::TYPE_PASS_FAIL => 'pass-fail',
112+
self::TYPE_SCORING => 'scoring',
113+
self::TYPE_MULTI_PASS => 'multi-pass',
114+
self::TYPE_INTERACTIVE => 'interactive',
115+
self::TYPE_SUBMIT_ANSWER => 'submit-answer',
116+
];
117+
118+
#[ORM\Column(options: ['comment' => 'Bitmask of problem types, default is pass-fail.'])]
115119
#[Serializer\Exclude]
116-
private bool $isMultipassProblem = false;
120+
private int $types = self::TYPE_PASS_FAIL;
117121

118122
#[ORM\Column(
119123
nullable: true,
@@ -287,26 +291,84 @@ public function getSpecialCompareArgs(): ?string
287291
return $this->special_compare_args;
288292
}
289293

290-
public function setCombinedRunCompare(bool $combinedRunCompare): Problem
294+
public function setTypesAsString(array $types): Problem
291295
{
292-
$this->combined_run_compare = $combinedRunCompare;
296+
$stringToTypes = array_flip($this->typesToString);
297+
$typeConstants = [];
298+
foreach ($types as $type) {
299+
if (!isset($stringToTypes[$type])) {
300+
throw new Exception("Unknown problem type: '$type', must be one of " . implode(', ', array_keys($stringToTypes)));
301+
}
302+
$typeConstants[$type] = $stringToTypes[$type];
303+
}
304+
$this->setTypes($typeConstants);
305+
293306
return $this;
294307
}
295308

296-
public function getCombinedRunCompare(): bool
309+
public function getTypesAsString(): string
310+
{
311+
$typeConstants = $this->getTypes();
312+
$typeStrings = [];
313+
foreach ($typeConstants as $type) {
314+
if (!isset($this->typesToString[$type])) {
315+
throw new Exception("Unknown problem type: '$type'");
316+
}
317+
$typeStrings[] = $this->typesToString[$type];
318+
}
319+
return implode(', ', $typeStrings);
320+
}
321+
322+
public function getTypes(): array
297323
{
298-
return $this->combined_run_compare;
324+
$ret = [];
325+
foreach (array_keys($this->typesToString) as $type) {
326+
if ($this->types & $type) {
327+
$ret[] = $type;
328+
}
329+
}
330+
return $ret;
299331
}
300332

301-
public function setMultipassProblem(bool $isMultipassProblem): Problem
333+
public function setTypes(array $types): Problem
302334
{
303-
$this->isMultipassProblem = $isMultipassProblem;
335+
$types = array_unique($types);
336+
$this->types = 0;
337+
foreach ($types as $type) {
338+
$this->types |= $type;
339+
}
340+
if ($this->types & self::TYPE_PASS_FAIL && $this->types & self::TYPE_SCORING) {
341+
throw new Exception("Invalid problem type: 'pass-fail' and 'scoring' are mutually exclusive.");
342+
}
343+
if ($this->types & self::TYPE_SUBMIT_ANSWER) {
344+
if ($this->types & self::TYPE_MULTI_PASS) {
345+
throw new Exception("Invalid problem type: 'submit-answer' and 'multi-pass' are mutually exclusive.");
346+
}
347+
if ($this->types & self::TYPE_INTERACTIVE) {
348+
throw new Exception("Invalid problem type: 'submit-answer' and 'interactive' are mutually exclusive.");
349+
}
350+
}
304351
return $this;
305352
}
306353

354+
public function isInteractiveProblem(): bool
355+
{
356+
return (bool)($this->types & self::TYPE_INTERACTIVE);
357+
}
358+
307359
public function isMultipassProblem(): bool
308360
{
309-
return $this->isMultipassProblem;
361+
return (bool)($this->types & self::TYPE_MULTI_PASS);
362+
}
363+
364+
public function isPassFailProblem(): bool
365+
{
366+
return (bool)($this->types & self::TYPE_PASS_FAIL);
367+
}
368+
369+
public function isScoringProblem(): bool
370+
{
371+
return (bool)($this->types & self::TYPE_SCORING);
310372
}
311373

312374
public function setMultipassLimit(?int $multipassLimit): Problem
@@ -317,7 +379,7 @@ public function setMultipassLimit(?int $multipassLimit): Problem
317379

318380
public function getMultipassLimit(): int
319381
{
320-
if ($this->isMultipassProblem) {
382+
if ($this->isMultipassProblem()) {
321383
return $this->multipassLimit ?? 2;
322384
}
323385
return 1;

webapp/src/Form/Type/ProblemType.php

+11-7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Doctrine\ORM\EntityRepository;
99
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
1010
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
11+
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
1112
use Symfony\Component\Form\Extension\Core\Type\FileType;
1213
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
1314
use Symfony\Component\Form\Extension\Core\Type\NumberType;
@@ -78,13 +79,16 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
7879
'label' => 'Compare script arguments',
7980
'required' => false,
8081
]);
81-
$builder->add('combinedRunCompare', CheckboxType::class, [
82-
'label' => 'Use run script as compare script.',
83-
'required' => false,
84-
]);
85-
$builder->add('multipassProblem', CheckboxType::class, [
86-
'label' => 'Multi-pass problem',
87-
'required' => false,
82+
$builder->add('types', ChoiceType::class, [
83+
'choices' => [
84+
'pass-fail' => Problem::TYPE_PASS_FAIL,
85+
'interactive' => Problem::TYPE_INTERACTIVE,
86+
'multipass' => Problem::TYPE_MULTI_PASS,
87+
'scoring' => Problem::TYPE_SCORING,
88+
'submit-answer' => Problem::TYPE_SUBMIT_ANSWER,
89+
],
90+
'multiple' => true,
91+
'required' => true,
8892
]);
8993
$builder->add('multipassLimit', IntegerType::class, [
9094
'label' => 'Multi-pass limit',

webapp/src/Service/DOMJudgeService.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -791,7 +791,7 @@ public function printFile(
791791
*/
792792
public function getSamplesZipContent(ContestProblem $contestProblem): string
793793
{
794-
if ($contestProblem->getProblem()->getCombinedRunCompare()) {
794+
if ($contestProblem->getProblem()->isInteractiveProblem()) {
795795
throw new NotFoundHttpException(sprintf('Problem p%d has no downloadable samples', $contestProblem->getProbid()));
796796
}
797797

@@ -892,7 +892,7 @@ public function getSamplesZipForContest(Contest $contest): StreamedResponse
892892
/** @var ContestProblem $problem */
893893
foreach ($contest->getProblems() as $problem) {
894894
// We don't include the samples for interactive problems.
895-
if (!$problem->getProblem()->getCombinedRunCompare()) {
895+
if (!$problem->getProblem()->isInteractiveProblem()) {
896896
$this->addSamplesToZip($zip, $problem, $problem->getShortname());
897897
}
898898

@@ -1452,7 +1452,7 @@ public function getCompareConfig(ContestProblem $problem): string
14521452
'script_memory_limit' => $this->config->get('script_memory_limit'),
14531453
'script_filesize_limit' => $this->config->get('script_filesize_limit'),
14541454
'compare_args' => $problem->getProblem()->getSpecialCompareArgs(),
1455-
'combined_run_compare' => $problem->getProblem()->getCombinedRunCompare(),
1455+
'combined_run_compare' => $problem->getProblem()->isInteractiveProblem(),
14561456
'hash' => $compareExecutable->getHash(),
14571457
]
14581458
);

webapp/src/Service/ImportProblemService.php

+20-4
Original file line numberDiff line numberDiff line change
@@ -204,10 +204,10 @@ public function importZippedProblem(
204204
// The same holds for the timelimit of the problem.
205205
if ($problem->getProbid()) {
206206
$problem
207+
->setTypesAsString(['pass-fail'])
207208
->setCompareExecutable()
208209
->setSpecialCompareArgs('')
209210
->setRunExecutable()
210-
->setCombinedRunCompare(false)
211211
->setMemlimit(null)
212212
->setOutputlimit(null)
213213
->setProblemStatementContent(null)
@@ -277,6 +277,15 @@ public function importZippedProblem(
277277
$yamlProblemProperties['name'] = $yamlData['name'];
278278
}
279279
}
280+
281+
if (isset($yamlData['type'])) {
282+
$types = explode(' ', $yamlData['type']);
283+
// Validation happens later when we set the properties.
284+
$yamlProblemProperties['typesAsString'] = $types;
285+
} else {
286+
$yamlProblemProperties['typesAsString'] = ['pass-fail'];
287+
}
288+
280289
if (isset($yamlData['validator_flags'])) {
281290
$yamlProblemProperties['special_compare_args'] = $yamlData['validator_flags'];
282291
}
@@ -290,7 +299,10 @@ public function importZippedProblem(
290299
}
291300

292301
if ($yamlData['validation'] == 'custom multi-pass') {
293-
$problem->setMultipassProblem(true);
302+
$yamlProblemProperties['typesAsString'][] = 'multi-pass';
303+
}
304+
if ($yamlData['validation'] == 'custom interactive') {
305+
$yamlProblemProperties['typesAsString'][] = 'interactive';
294306
}
295307
}
296308

@@ -307,7 +319,12 @@ public function importZippedProblem(
307319
}
308320

309321
foreach ($yamlProblemProperties as $key => $value) {
310-
$propertyAccessor->setValue($problem, $key, $value);
322+
try {
323+
$propertyAccessor->setValue($problem, $key, $value);
324+
} catch (Exception $e) {
325+
$messages['danger'][] = sprintf('Error: problem.%s: %s', $key, $e->getMessage());
326+
return null;
327+
}
311328
}
312329
}
313330
}
@@ -1020,7 +1037,6 @@ private function searchAndAddValidator(ZipArchive $zip, ?array &$messages, strin
10201037
$this->em->persist($executable);
10211038

10221039
if ($combinedRunCompare) {
1023-
$problem->setCombinedRunCompare(true);
10241040
$problem->setRunExecutable($executable);
10251041
} else {
10261042
$problem->setCompareExecutable($executable);

webapp/templates/jury/problem.html.twig

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
{% endif %}
8787
</td>
8888
</tr>
89-
{% if problem.combinedRunCompare %}
89+
{% if problem.isInteractiveProblem %}
9090
<tr>
9191
<th>Compare script</th>
9292
<td>Run script combines run and compare script.</td>

0 commit comments

Comments
 (0)