Skip to content

Commit b977e27

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 b977e27

File tree

10 files changed

+172
-55
lines changed

10 files changed

+172
-55
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' => implode(", ", $p->getTypes())],
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' => implode(', ', $problem->getTypes()),
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

+72-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,15 @@ 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+
#[ORM\Column(options: ['comment' => 'Bitmask of problem types, default is pass-fail.'])]
115111
#[Serializer\Exclude]
116-
private bool $isMultipassProblem = false;
112+
private int $types = self::TYPE_PASS_FAIL;
117113

118114
#[ORM\Column(
119115
nullable: true,
@@ -287,26 +283,82 @@ public function getSpecialCompareArgs(): ?string
287283
return $this->special_compare_args;
288284
}
289285

290-
public function setCombinedRunCompare(bool $combinedRunCompare): Problem
286+
public function setTypes(array $types): Problem
291287
{
292-
$this->combined_run_compare = $combinedRunCompare;
288+
$types = array_unique($types);
289+
290+
$this->types = 0;
291+
foreach ($types as $type) {
292+
if ($type === 'pass-fail') {
293+
$this->types |= self::TYPE_PASS_FAIL;
294+
} elseif ($type === 'scoring') {
295+
$this->types |= self::TYPE_SCORING;
296+
} elseif ($type === 'multi-pass') {
297+
$this->types |= self::TYPE_MULTI_PASS;
298+
} elseif ($type === 'interactive') {
299+
$this->types |= self::TYPE_INTERACTIVE;
300+
}
301+
}
293302
return $this;
294303
}
295304

296-
public function getCombinedRunCompare(): bool
305+
public function getTypes(): array
306+
{
307+
$types = [];
308+
if ($this->isPassFailProblem()) {
309+
$types[] = 'pass-fail';
310+
}
311+
if ($this->isScoringProblem()) {
312+
$types[] = 'scoring';
313+
}
314+
if ($this->isMultipassProblem()) {
315+
$types[] = 'multi-pass';
316+
}
317+
if ($this->isInteractiveProblem()) {
318+
$types[] = 'interactive';
319+
}
320+
321+
return $types;
322+
}
323+
324+
public function getTypesForForm(): array
297325
{
298-
return $this->combined_run_compare;
326+
$ret = [];
327+
foreach ([self::TYPE_PASS_FAIL, self::TYPE_SCORING, self::TYPE_MULTI_PASS, self::TYPE_INTERACTIVE, self::TYPE_SUBMIT_ANSWER] as $type) {
328+
if ($this->types & $type) {
329+
$ret[] = $type;
330+
}
331+
}
332+
return $ret;
299333
}
300334

301-
public function setMultipassProblem(bool $isMultipassProblem): Problem
335+
public function setTypesForForm(array $types): Problem
302336
{
303-
$this->isMultipassProblem = $isMultipassProblem;
337+
$this->types = 0;
338+
foreach ($types as $type) {
339+
$this->types |= $type;
340+
}
304341
return $this;
305342
}
306343

344+
public function isInteractiveProblem(): bool
345+
{
346+
return (bool)($this->types & self::TYPE_INTERACTIVE);
347+
}
348+
307349
public function isMultipassProblem(): bool
308350
{
309-
return $this->isMultipassProblem;
351+
return (bool)($this->types & self::TYPE_MULTI_PASS);
352+
}
353+
354+
public function isPassFailProblem(): bool
355+
{
356+
return (bool)($this->types & self::TYPE_PASS_FAIL);
357+
}
358+
359+
public function isScoringProblem(): bool
360+
{
361+
return (bool)($this->types & self::TYPE_SCORING);
310362
}
311363

312364
public function setMultipassLimit(?int $multipassLimit): Problem
@@ -317,7 +369,7 @@ public function setMultipassLimit(?int $multipassLimit): Problem
317369

318370
public function getMultipassLimit(): int
319371
{
320-
if ($this->isMultipassProblem) {
372+
if ($this->isMultipassProblem()) {
321373
return $this->multipassLimit ?? 2;
322374
}
323375
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('typesForForm', 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

+34-3
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+
->setTypes(['pass-fail'])
207208
->setCompareExecutable()
208209
->setSpecialCompareArgs('')
209210
->setRunExecutable()
210-
->setCombinedRunCompare(false)
211211
->setMemlimit(null)
212212
->setOutputlimit(null)
213213
->setProblemStatementContent(null)
@@ -277,6 +277,35 @@ public function importZippedProblem(
277277
$yamlProblemProperties['name'] = $yamlData['name'];
278278
}
279279
}
280+
281+
if (isset($yamlData['type'])) {
282+
$types = explode(' ', $yamlData['type']);
283+
foreach ($types as $type) {
284+
$allowedProblemTypes = ['pass-fail', 'multi-pass', 'scoring', 'interactive', 'submit-answer'];
285+
if (!in_array($type, $allowedProblemTypes)) {
286+
$messages['danger'][] = "Invalid problem type: '$type', must be one of " . implode(', ', $allowedProblemTypes);
287+
return null;
288+
}
289+
}
290+
if (in_array('pass-fail', $types) && in_array('scoring', $types)) {
291+
$messages['danger'][] = "Invalid problem type: 'pass-fail' and 'scoring' are mutually exclusive.";
292+
return null;
293+
}
294+
if (in_array('submit-answer', $types)) {
295+
if (in_array('multi-pass', $types)) {
296+
$messages['danger'][] = "Invalid problem type: 'submit-answer' and 'multi-pass' are mutually exclusive.";
297+
return null;
298+
}
299+
if (in_array('interactive', $types)) {
300+
$messages['danger'][] = "Invalid problem type: 'submit-answer' and 'interactive' are mutually exclusive.";
301+
return null;
302+
}
303+
}
304+
$yamlProblemProperties['types'] = $types;
305+
} else {
306+
$yamlProblemProperties['types'] = ['pass-fail'];
307+
}
308+
280309
if (isset($yamlData['validator_flags'])) {
281310
$yamlProblemProperties['special_compare_args'] = $yamlData['validator_flags'];
282311
}
@@ -290,7 +319,10 @@ public function importZippedProblem(
290319
}
291320

292321
if ($yamlData['validation'] == 'custom multi-pass') {
293-
$problem->setMultipassProblem(true);
322+
$yamlProblemProperties['types'][] = 'multi-pass';
323+
}
324+
if ($yamlData['validation'] == 'custom interactive') {
325+
$yamlProblemProperties['types'][] = 'interactive';
294326
}
295327
}
296328

@@ -1020,7 +1052,6 @@ private function searchAndAddValidator(ZipArchive $zip, ?array &$messages, strin
10201052
$this->em->persist($executable);
10211053

10221054
if ($combinedRunCompare) {
1023-
$problem->setCombinedRunCompare(true);
10241055
$problem->setRunExecutable($executable);
10251056
} else {
10261057
$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)