diff --git a/docker-compose.yml b/docker-compose.yml index 181596b96c..64e8386509 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,7 @@ services: volumes: - /var/lib/mysql domjudge: - image: docker.io/domjudge/domjudge-contributor + image: as6325400/domjudge-contributor:8.3.1 hostname: domjudge-contributor volumes: - /sys/fs/cgroup:/sys/fs/cgroup diff --git a/etc/db-config.yaml b/etc/db-config.yaml index 9c6b0866b4..3f33a7a6e4 100644 --- a/etc/db-config.yaml +++ b/etc/db-config.yaml @@ -244,6 +244,11 @@ 2: always regex: /^\d+$/ error_message: A value between 0 and 2 is required. + - name: show_test_results + type: bool + default_value: false + public: true + description: Show what kinds of testcases to user? - name: show_sample_output type: bool default_value: false diff --git a/judge/judgedaemon.main.php b/judge/judgedaemon.main.php index 1d0eb63560..876ac56d71 100644 --- a/judge/judgedaemon.main.php +++ b/judge/judgedaemon.main.php @@ -1424,12 +1424,18 @@ function judge(array $judgeTask): bool // Try to read metadata from file $runtime = null; + $optscore = null; $metadata = read_metadata($testcasedir . '/program.meta'); + $compareMeta = read_metadata($testcasedir.'/compare.meta'); if (isset($metadata['time-used'])) { $runtime = @$metadata[$metadata['time-used']]; } + if (isset($compareMeta['opt-score'])) { + $optscore = @$compareMeta['opt-score']; + } + if ($result === 'compare-error') { if ($combined_run_compare) { logmsg(LOG_ERR, "comparing failed for combined run/compare script '" . $judgeTask['run_script_id'] . "'"); @@ -1446,6 +1452,7 @@ function judge(array $judgeTask): bool $new_judging_run = [ 'runresult' => urlencode($result), 'runtime' => urlencode((string)$runtime), + 'optscore' => urlencode((string)$optscore), 'output_run' => rest_encode_file($testcasedir . '/program.out', $output_storage_limit), 'output_error' => rest_encode_file($testcasedir . '/program.err', $output_storage_limit), 'output_system' => rest_encode_file($testcasedir . '/system.out', $output_storage_limit), diff --git a/judge/testcase_run.sh b/judge/testcase_run.sh index 0f83e1f7ec..2bdbacd24f 100755 --- a/judge/testcase_run.sh +++ b/judge/testcase_run.sh @@ -233,6 +233,19 @@ if [ $COMBINED_RUN_COMPARE -eq 0 ]; then -f $SCRIPTFILELIMIT -s $SCRIPTFILELIMIT -M compare.meta -- \ "$COMPARE_SCRIPT" testdata.in testdata.out feedback/ $COMPARE_ARGS < program.out \ >compare.tmp 2>&1 + + # match optscore + if grep -q '^OPT_SCORE=' compare.tmp ; then + score="$(grep -m1 '^OPT_SCORE=' compare.tmp | cut -d= -f2-)" + case "$score" in + ''|*[!0-9.-]*|*.*.*|*.-*) + echo "Invalid OPT_SCORE value: $score" >&2 + ;; + *) + echo "opt-score: $score" >> compare.meta + ;; + esac + fi fi # Make sure that all feedback files are owned by the current diff --git a/webapp/migrations/Version20250517062145.php b/webapp/migrations/Version20250517062145.php new file mode 100644 index 0000000000..6eb36b627b --- /dev/null +++ b/webapp/migrations/Version20250517062145.php @@ -0,0 +1,36 @@ +addSql('ALTER TABLE contest ADD similarity_as_score_tiebreaker TINYINT(1) DEFAULT 0 NOT NULL COMMENT \'Use similarity-based scoring instead of exact match\', ADD similarity_order VARCHAR(10) DEFAULT \'asc\' NOT NULL COMMENT \'Order to apply for similarity-based scoring: asc or desc\''); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE contest DROP similarity_as_score_tiebreaker, DROP similarity_order'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/migrations/Version20250517084703.php b/webapp/migrations/Version20250517084703.php new file mode 100644 index 0000000000..9053657d43 --- /dev/null +++ b/webapp/migrations/Version20250517084703.php @@ -0,0 +1,36 @@ +addSql('ALTER TABLE contest ADD opt_score_as_score_tiebreaker TINYINT(1) DEFAULT 0 NOT NULL COMMENT \'Use objective-score–based ranking instead of exact match\', ADD opt_score_order VARCHAR(10) DEFAULT \'asc\' NOT NULL COMMENT \'Order to apply for objective score: asc(smaller-better) or desc(larger-better)\', DROP similarity_as_score_tiebreaker, DROP similarity_order'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE contest ADD similarity_as_score_tiebreaker TINYINT(1) DEFAULT 0 NOT NULL COMMENT \'Use similarity-based scoring instead of exact match\', ADD similarity_order VARCHAR(10) DEFAULT \'asc\' NOT NULL COMMENT \'Order to apply for similarity-based scoring: asc or desc\', DROP opt_score_as_score_tiebreaker, DROP opt_score_order'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/migrations/Version20250517085220.php b/webapp/migrations/Version20250517085220.php new file mode 100644 index 0000000000..ac2882ac04 --- /dev/null +++ b/webapp/migrations/Version20250517085220.php @@ -0,0 +1,36 @@ +addSql('ALTER TABLE contest CHANGE opt_score_as_score_tiebreaker opt_score_as_score_tiebreaker TINYINT(1) DEFAULT 0 NOT NULL COMMENT \'Use optimization score ranking instead of exact match\''); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE contest CHANGE opt_score_as_score_tiebreaker opt_score_as_score_tiebreaker TINYINT(1) DEFAULT 0 NOT NULL COMMENT \'Use objective-score–based ranking instead of exact match\''); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/migrations/Version20250517145253.php b/webapp/migrations/Version20250517145253.php new file mode 100644 index 0000000000..e7ec7265dd --- /dev/null +++ b/webapp/migrations/Version20250517145253.php @@ -0,0 +1,36 @@ +addSql('ALTER TABLE judging_run ADD optscore DOUBLE PRECISION DEFAULT NULL COMMENT \'optimization score\''); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE judging_run DROP optscore'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/migrations/Version20250519064616.php b/webapp/migrations/Version20250519064616.php new file mode 100644 index 0000000000..284f0a47b6 --- /dev/null +++ b/webapp/migrations/Version20250519064616.php @@ -0,0 +1,36 @@ +addSql('ALTER TABLE scorecache ADD optscore_max_restricted DOUBLE PRECISION DEFAULT \'0\' NOT NULL COMMENT \'Max optscore (restricted audience)\', ADD optscore_max_public DOUBLE PRECISION DEFAULT \'0\' NOT NULL COMMENT \'Max optscore (public audience)\', ADD optscore_min_restricted DOUBLE PRECISION DEFAULT \'0\' NOT NULL COMMENT \'Min optscore (restricted audience)\', ADD optscore_min_public DOUBLE PRECISION DEFAULT \'0\' NOT NULL COMMENT \'Min optscore (public audience)\''); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE scorecache DROP optscore_max_restricted, DROP optscore_max_public, DROP optscore_min_restricted, DROP optscore_min_public'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/migrations/Version20250519122129.php b/webapp/migrations/Version20250519122129.php new file mode 100644 index 0000000000..334eb3ecbb --- /dev/null +++ b/webapp/migrations/Version20250519122129.php @@ -0,0 +1,36 @@ +addSql('ALTER TABLE scorecache CHANGE optscore_max_restricted optscore_max_restricted DOUBLE PRECISION DEFAULT \'0\' COMMENT \'Max optscore (restricted audience)\', CHANGE optscore_max_public optscore_max_public DOUBLE PRECISION DEFAULT \'0\' COMMENT \'Max optscore (public audience)\', CHANGE optscore_min_restricted optscore_min_restricted DOUBLE PRECISION DEFAULT \'0\' COMMENT \'Min optscore (restricted audience)\', CHANGE optscore_min_public optscore_min_public DOUBLE PRECISION DEFAULT \'0\' COMMENT \'Min optscore (public audience)\''); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE scorecache CHANGE optscore_max_restricted optscore_max_restricted DOUBLE PRECISION DEFAULT \'0\' NOT NULL COMMENT \'Max optscore (restricted audience)\', CHANGE optscore_max_public optscore_max_public DOUBLE PRECISION DEFAULT \'0\' NOT NULL COMMENT \'Max optscore (public audience)\', CHANGE optscore_min_restricted optscore_min_restricted DOUBLE PRECISION DEFAULT \'0\' NOT NULL COMMENT \'Min optscore (restricted audience)\', CHANGE optscore_min_public optscore_min_public DOUBLE PRECISION DEFAULT \'0\' NOT NULL COMMENT \'Min optscore (public audience)\''); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/migrations/Version20250519130014.php b/webapp/migrations/Version20250519130014.php new file mode 100644 index 0000000000..1c2a4e3379 --- /dev/null +++ b/webapp/migrations/Version20250519130014.php @@ -0,0 +1,36 @@ +addSql('ALTER TABLE rankcache ADD totaloptscore_max_restricted DOUBLE PRECISION DEFAULT \'0\' NOT NULL COMMENT \'Total max optscore (restricted audience)\', ADD optscore_max_public DOUBLE PRECISION DEFAULT \'0\' NOT NULL COMMENT \'Max optscore (public audience)\', ADD optscore_min_restricted DOUBLE PRECISION DEFAULT \'0\' NOT NULL COMMENT \'Min optscore (restricted audience)\', ADD optscore_min_public DOUBLE PRECISION DEFAULT \'0\' NOT NULL COMMENT \'Min optscore (public audience)\''); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE rankcache DROP totaloptscore_max_restricted, DROP optscore_max_public, DROP optscore_min_restricted, DROP optscore_min_public'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/migrations/Version20250519134327.php b/webapp/migrations/Version20250519134327.php new file mode 100644 index 0000000000..d17f59de74 --- /dev/null +++ b/webapp/migrations/Version20250519134327.php @@ -0,0 +1,36 @@ +addSql('ALTER TABLE rankcache ADD totaloptscore_max_public DOUBLE PRECISION DEFAULT \'0\' NOT NULL COMMENT \'Total max optscore (public audience)\', ADD totaloptscore_min_restricted DOUBLE PRECISION DEFAULT \'0\' NOT NULL COMMENT \'Total min optscore (restricted audience)\', ADD totaloptscore_min_public DOUBLE PRECISION DEFAULT \'0\' NOT NULL COMMENT \'Total min optscore (public audience)\', DROP optscore_max_public, DROP optscore_min_restricted, DROP optscore_min_public'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE rankcache ADD optscore_max_public DOUBLE PRECISION DEFAULT \'0\' NOT NULL COMMENT \'Max optscore (public audience)\', ADD optscore_min_restricted DOUBLE PRECISION DEFAULT \'0\' NOT NULL COMMENT \'Min optscore (restricted audience)\', ADD optscore_min_public DOUBLE PRECISION DEFAULT \'0\' NOT NULL COMMENT \'Min optscore (public audience)\', DROP totaloptscore_max_public, DROP totaloptscore_min_restricted, DROP totaloptscore_min_public'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/src/Controller/API/JudgehostController.php b/webapp/src/Controller/API/JudgehostController.php index 276519a9ea..f0ad313ebe 100644 --- a/webapp/src/Controller/API/JudgehostController.php +++ b/webapp/src/Controller/API/JudgehostController.php @@ -608,6 +608,12 @@ public function addDebugInfo( description: 'The (base64-encoded) metadata', type: 'string' ), + new OA\Property( + property: 'optscore', + description: 'Optional optimisation score (float)', + type: 'number', + format: 'float' + ), ] ) ) @@ -644,6 +650,7 @@ public function addJudgingRunAction( $teamMessage = $request->request->get('team_message'); $metadata = $request->request->get('metadata'); $testcasedir = $request->request->get('testcasedir'); + $optScore = $request->request->get('optscore'); $judgehost = $this->em->getRepository(Judgehost::class)->findOneBy(['hostname' => $hostname]); if (!$judgehost) { @@ -651,7 +658,7 @@ public function addJudgingRunAction( } $hasFinalResult = $this->addSingleJudgingRun($judgeTaskId, $hostname, $runResult, $runTime, - $outputSystem, $outputError, $outputDiff, $outputRun, $teamMessage, $metadata, $testcasedir); + $outputSystem, $outputError, $outputDiff, $outputRun, $teamMessage, $metadata, $testcasedir, $optScore); $judgehost = $this->em->getRepository(Judgehost::class)->findOneBy(['hostname' => $hostname]); $judgehost->setPolltime(Utils::now()); $this->em->flush(); @@ -916,7 +923,8 @@ private function addSingleJudgingRun( string $outputRun, ?string $teamMessage, string $metadata, - ?string $testcasedir + ?string $testcasedir, + ?string $optScore ): bool { $resultsRemap = $this->config->get('results_remap'); $resultsPrio = $this->config->get('results_prio'); @@ -937,7 +945,8 @@ private function addSingleJudgingRun( $outputRun, $teamMessage, $metadata, - $testcasedir + $testcasedir, + $optScore ) { $judgingRun = $this->em->getRepository(JudgingRun::class)->findOneBy( ['judgetaskid' => $judgeTaskId]); @@ -963,6 +972,10 @@ private function addSingleJudgingRun( $judgingRunOutput->setTeamMessage(base64_decode($teamMessage)); } + if ($optScore !== null and $optScore !== '') { + $judgingRun->setOptScore((float)$optScore); + } + $judging = $judgingRun->getJudging(); $this->maybeUpdateActiveJudging($judging); $this->em->flush(); diff --git a/webapp/src/Controller/API/ScoreboardController.php b/webapp/src/Controller/API/ScoreboardController.php index f4c986ac39..d160c0107f 100644 --- a/webapp/src/Controller/API/ScoreboardController.php +++ b/webapp/src/Controller/API/ScoreboardController.php @@ -180,6 +180,11 @@ public function getScoreboardAction( numSolved: $teamScore->numPoints, totalRuntime: $teamScore->totalRuntime, ); + } else if($contest->getOptScoreAsScoreTiebreaker()) { + $score = new Score( + numSolved: $teamScore->numPoints, + optMaxScore: $teamScore->$totalOptscore, + ); } else { $score = new Score( numSolved: $teamScore->numPoints, diff --git a/webapp/src/Controller/Team/SubmissionController.php b/webapp/src/Controller/Team/SubmissionController.php index e6cfbf223f..28709746a8 100644 --- a/webapp/src/Controller/Team/SubmissionController.php +++ b/webapp/src/Controller/Team/SubmissionController.php @@ -133,6 +133,7 @@ public function viewAction(Request $request, int $submitId): Response ->setParameter('team', $team) ->getQuery() ->getOneOrNullResult(); + $showTestResults = (bool)$this->config->get('show_test_results'); // Update seen status when viewing submission. if ($judging && $judging->getSubmission()->getSubmittime() < $contest->getEndtime() && @@ -183,6 +184,23 @@ public function viewAction(Request $request, int $submitId): Response ->getResult(); } + $testcasesruns = []; + if ($showTestResults){ + $queryBuilder = $this->em->createQueryBuilder() + ->from(Testcase::class, 't') + ->join('t.content', 'tc') + ->leftJoin('t.judging_runs', 'jr', Join::WITH, 'jr.judging = :judging') + ->leftJoin('jr.output', 'jro') + ->select('t', 'jr', 'tc') + ->andWhere('t.problem = :problem') + ->setParameter('judging', $judging) + ->setParameter('problem', $judging->getSubmission()->getProblem()) + ->orderBy('t.ranknumber'); + $testcasesruns = $queryBuilder + ->getQuery() + ->getResult(); + } + $actuallyShowCompile = $showCompile == self::ALWAYS_SHOW_COMPILE_OUTPUT || ($showCompile == self::ONLY_SHOW_COMPILE_OUTPUT_ON_ERROR && $judging->getResult() === 'compiler-error'); @@ -194,6 +212,8 @@ public function viewAction(Request $request, int $submitId): Response 'showSampleOutput' => $showSampleOutput, 'runs' => $runs, 'showTooLateResult' => $showTooLateResult, + 'showTestResults' => $showTestResults, + 'testcasesruns' => $testcasesruns, ]; if ($actuallyShowCompile) { $data['size'] = 'xl'; diff --git a/webapp/src/Entity/Contest.php b/webapp/src/Entity/Contest.php index 9c5cfe6ab3..612399c6c4 100644 --- a/webapp/src/Entity/Contest.php +++ b/webapp/src/Entity/Contest.php @@ -281,6 +281,24 @@ class Contest extends BaseApiEntity implements AssetEntityInterface #[Serializer\Groups([ARC::GROUP_NONSTRICT])] private bool $runtime_as_score_tiebreaker = false; + #[ORM\Column( + options: ['comment' => 'Use optimization score ranking instead of exact match', 'default' => 0] + )] + #[Serializer\Groups([ARC::GROUP_NONSTRICT])] + private bool $opt_score_as_score_tiebreaker = false; + + #[ORM\Column( + type: 'string', + length: 10, + nullable: false, + options: [ + 'default' => 'asc', + 'comment' => 'Order to apply for objective score: asc(smaller-better) or desc(larger-better)' + ] + )] + #[Serializer\Groups([ARC::GROUP_NONSTRICT])] + private string $opt_score_order = 'asc'; + #[ORM\Column( options: ['comment' => 'Is this contest visible for the public?', 'default' => 1] )] @@ -767,6 +785,28 @@ public function getRuntimeAsScoreTiebreaker(): bool return $this->runtime_as_score_tiebreaker; } + public function setOptScoreAsScoreTiebreaker(bool $optScoreAsScoreTiebreaker): Contest + { + $this->opt_score_as_score_tiebreaker = $optScoreAsScoreTiebreaker; + return $this; + } + + public function getOptScoreAsScoreTiebreaker(): bool + { + return $this->opt_score_as_score_tiebreaker; + } + + public function setOptScoreOrder(string $order): Contest + { + $this->opt_score_order = $order; + return $this; + } + + public function getOptScoreOrder(): string + { + return $this->opt_score_order; + } + public function setMedalsEnabled(bool $medalsEnabled): Contest { $this->medalsEnabled = $medalsEnabled; @@ -1302,6 +1342,12 @@ public function validate(ExecutionContextInterface $context): void } } + if ($this->runtime_as_score_tiebreaker && $this->opt_score_as_score_tiebreaker) { + $context->buildViolation('Cannot enable both runtime and optimization score tiebreakers at the same time.') + ->atPath('optScoreAsScoreTiebreaker') + ->addViolation(); + } + /** @var ContestProblem $problem */ foreach ($this->problems as $idx => $problem) { // Check if the problem ID is unique. diff --git a/webapp/src/Entity/Judging.php b/webapp/src/Entity/Judging.php index 19b81e29ce..c0d3bf3a7e 100644 --- a/webapp/src/Entity/Judging.php +++ b/webapp/src/Entity/Judging.php @@ -179,6 +179,21 @@ public function getMaxRuntime(): ?float return $max; } + public function getSumOptScore(): ?float + { + if ($this->runs->isEmpty()) { + return null; + } + $sum = 0; + foreach ($this->runs as $run) { + if ($run->getOptscore() === null) { + return null; + } + $sum += $run->getOptscore(); + } + return $sum; + } + public function getSumRuntime(): float { $sum = 0; diff --git a/webapp/src/Entity/JudgingRun.php b/webapp/src/Entity/JudgingRun.php index 2480a215f2..c1270f3399 100644 --- a/webapp/src/Entity/JudgingRun.php +++ b/webapp/src/Entity/JudgingRun.php @@ -50,6 +50,13 @@ class JudgingRun extends BaseApiEntity #[Serializer\Exclude] private ?float $runtime = null; + #[ORM\Column( + nullable: true, + options: ['comment' => 'optimization score'] + )] + #[Serializer\Exclude] + private ?float $optscore = null; + #[ORM\Column( type: 'decimal', precision: 32, @@ -151,6 +158,20 @@ public function getRuntime(): ?float return Utils::roundedFloat($this->runtime); } + public function setOptscore(float $optscore): JudgingRun + { + $this->optscore = $optscore; + return $this; + } + + #[Serializer\VirtualProperty] + #[Serializer\SerializedName('opt_score')] + #[Serializer\Type('float')] + public function getOptscore(): ?float + { + return Utils::roundedFloat($this->optscore); + } + public function setEndtime(string|float $endtime): JudgingRun { $this->endtime = $endtime; diff --git a/webapp/src/Entity/RankCache.php b/webapp/src/Entity/RankCache.php index dfe7f6d9e4..2bfd42b802 100644 --- a/webapp/src/Entity/RankCache.php +++ b/webapp/src/Entity/RankCache.php @@ -52,6 +52,18 @@ class RankCache #[ORM\Column(options: ['comment' => 'Total runtime in milliseconds (public)', 'default' => 0])] private int $totalruntime_public = 0; + #[ORM\Column(options: ['comment' => 'Total max optscore (restricted audience)', 'default' => 0])] + private float $totaloptscore_max_restricted = 0; + + #[ORM\Column(options: ['comment' => 'Total max optscore (public audience)', 'default' => 0])] + private float $totaloptscore_max_public = 0; + + #[ORM\Column(options: ['comment' => 'Total min optscore (restricted audience)', 'default' => 0])] + private float $totaloptscore_min_restricted = 0; + + #[ORM\Column(options: ['comment' => 'Total min optscore (public audience)', 'default' => 0])] + private float $totaloptscore_min_public = 0; + #[ORM\Id] #[ORM\ManyToOne] #[ORM\JoinColumn(name: 'cid', referencedColumnName: 'cid', onDelete: 'CASCADE')] @@ -128,6 +140,72 @@ public function getTotalruntimePublic(): int return $this->totalruntime_public; } + public function setTotaloptscoreMaxRestricted(float $totaloptscoreMaxRestricted): RankCache + { + $this->totaloptscore_max_restricted = $totaloptscoreMaxRestricted; + return $this; + } + + public function getTotaloptscoreMaxRestricted(): float + { + return $this->totaloptscore_max_restricted; + } + + public function setTotalOptscoreMaxPublic(float $totalOptscoreMaxPublic): RankCache + { + $this->totaloptscore_max_public = $totalOptscoreMaxPublic; + return $this; + } + + public function getTotalOptscoreMaxPublic(): float + { + return $this->totaloptscore_max_public; + } + + public function setTotalOptscoreMinRestricted(float $totalOptscoreMinRestricted): RankCache + { + $this->totaloptscore_min_restricted = $totalOptscoreMinRestricted; + return $this; + } + + public function getTotalOptscoreMinRestricted(): float + { + return $this->totaloptscore_min_restricted; + } + + public function setTotalOptscoreMinPublic(float $totalOptscoreMinPublic): RankCache + { + $this->totaloptscore_min_public = $totalOptscoreMinPublic; + return $this; + } + + public function getTotalOptscoreMinPublic(): float + { + return $this->totaloptscore_min_public; + } + + public function getTotalOptscore(bool $restricted): float + { + if ($this->contest->getOptScoreOrder() == 'asc') return $restricted + ? $this->getTotalOptscoreMinRestricted() + : $this->getTotalOptscoreMinPublic(); + else return $restricted + ? $this->getTotalOptscoreMaxRestricted() + : $this->getTotalOptscoreMaxPublic(); + } + + public function getTotalOptscoreRestricted(): float + { + if ($this->contest->getOptScoreOrder() == 'asc') return $this->getTotalOptscoreMinRestricted(); + else return $this->getTotalOptscoreMaxRestricted(); + } + + public function getTotalOptscorePublic(): float + { + if ($this->contest->getOptScoreOrder() == 'asc') return $this->getTotalOptscoreMinPublic(); + else return $this->getTotalOptscoreMaxPublic(); + } + public function setContest(?Contest $contest = null): RankCache { $this->contest = $contest; diff --git a/webapp/src/Entity/ScoreCache.php b/webapp/src/Entity/ScoreCache.php index 67aa8bcf0b..c4a254c4ab 100644 --- a/webapp/src/Entity/ScoreCache.php +++ b/webapp/src/Entity/ScoreCache.php @@ -57,6 +57,46 @@ class ScoreCache ])] private int $runtime_restricted = 0; + #[ORM\Column( + type: 'float', + nullable: true, + options: [ + 'comment' => 'Max optscore (restricted audience)', + 'default' => 0, + ] + )] + private ?float $optscore_max_restricted = null; + + #[ORM\Column( + type: 'float', + nullable: true, + options: [ + 'comment' => 'Max optscore (public audience)', + 'default' => 0, + ] + )] + private ?float $optscore_max_public = null; + + #[ORM\Column( + type: 'float', + nullable: true, + options: [ + 'comment' => 'Min optscore (restricted audience)', + 'default' => 0, + ] + )] + private ?float $optscore_min_restricted = null; + + #[ORM\Column( + type: 'float', + nullable: true, + options: [ + 'comment' => 'Min optscore (public audience)', + 'default' => 0, + ] + )] + private ?float $optscore_min_public = null; + #[ORM\Column(options: [ 'comment' => 'Number of submissions made (public)', 'unsigned' => true, @@ -163,6 +203,50 @@ public function getRuntimeRestricted(): int { return $this->runtime_restricted; } + + public function setOptScoreMaxRestricted(float $optscoreMaxRestricted): ScoreCache + { + $this->optscore_max_restricted = $optscoreMaxRestricted; + return $this; + } + + public function getOptScoreMaxRestricted(): float + { + return $this->optscore_max_restricted ?? 0; + } + + public function setOptScoreMaxPublic(float $optscoreMaxPublic): ScoreCache + { + $this->optscore_max_public = $optscoreMaxPublic; + return $this; + } + + public function getOptScoreMaxPublic(): float + { + return $this->optscore_max_public ?? 0; + } + + public function setOptScoreMinRestricted(float $optscoreMinRestricted): ScoreCache + { + $this->optscore_min_restricted = $optscoreMinRestricted; + return $this; + } + + public function getOptScoreMinRestricted(): float + { + return $this->optscore_min_restricted ?? 0; + } + + public function setOptScoreMinPublic(float $optscoreMinPublic): ScoreCache + { + $this->optscore_min_public = $optscoreMinPublic; + return $this; + } + + public function getOptScoreMinPublic(): float + { + return $this->optscore_min_public ?? 0; + } public function setSubmissionsPublic(int $submissionsPublic): ScoreCache { @@ -287,4 +371,20 @@ public function getIsCorrect(bool $restricted): bool { return $restricted ? $this->getIsCorrectRestricted() : $this->getIsCorrectPublic(); } + + public function getMaxOptscore(bool $restricted): float + { + return $restricted ? $this->getOptScoreMaxRestricted() : $this->getOptScoreMaxPublic(); + } + + public function getMinOptscore(bool $restricted): float + { + return $restricted ? $this->getOptScoreMinRestricted() : $this->getOptScoreMinPublic(); + } + + public function getOptscore(bool $restricted): float + { + if ($this->contest->getOptScoreOrder() == 'asc') return $this->getMinOptscore($restricted); + else return $this->getMaxOptscore($restricted); + } } diff --git a/webapp/src/Form/Type/ContestType.php b/webapp/src/Form/Type/ContestType.php index f186bb8d55..1d1d9e9c87 100644 --- a/webapp/src/Form/Type/ContestType.php +++ b/webapp/src/Form/Type/ContestType.php @@ -101,6 +101,19 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ], 'help' => 'Enable this to show runtimes in seconds on scoreboard and use them as tiebreaker instead of penalty. The runtime of a submission is the maximum over all testcases.', ]); + $builder->add('optScoreAsScoreTiebreaker', ChoiceType::class, [ + 'expanded' => true, + 'choices' => ['Yes' => true, 'No' => false], + 'help' => 'Enable this to rank submissions by optimization score instead of penalty.', + ]); + $builder->add('optScoreOrder', ChoiceType::class, [ + 'label' => 'optimization score ranking order', + 'choices' => [ + 'Smaller is better (asc)' => 'asc', + 'Larger is better (desc)' => 'desc', + ], + 'required' => false, + ]); $builder->add('medalsEnabled', ChoiceType::class, [ 'expanded' => true, 'choices' => [ diff --git a/webapp/src/Service/ScoreboardService.php b/webapp/src/Service/ScoreboardService.php index 37da962bdb..25a731cb6b 100644 --- a/webapp/src/Service/ScoreboardService.php +++ b/webapp/src/Service/ScoreboardService.php @@ -139,24 +139,44 @@ public function calculateTeamRank( $totalTime = $contest->getRuntimeAsScoreTiebreaker() ? $rankCache->getTotalruntimeRestricted() : $rankCache->getTotaltimeRestricted(); } $timeType = $contest->getRuntimeAsScoreTiebreaker() ? 'runtime' : 'time'; + $useOpt = $contest->getOptScoreAsScoreTiebreaker(); + $optMode = $contest->getOptScoreOrder() === 'asc' ? 'min' : 'max'; $sortOrder = $team->getCategory()->getSortorder(); + if ($useOpt) { + $tieField = "r.totaloptscore_{$optMode}_{$variant}"; + $tieOp = $optMode === 'min' ? '<' : '>'; + $tieValue = $rankCache + ? $rankCache->getTotalOptscore($restricted) + : 0; + $tieValue = $tieValue ?? 0; + } else { + $type = $contest->getRuntimeAsScoreTiebreaker() ? 'runtime' : 'time'; + $tieField = "r.total{$type}_{$variant}"; + $tieOp = '<'; + $tieValue = $totalTime; + } + // Number of teams that definitely ranked higher. - $better = $this->em->createQueryBuilder() + $better = (int) $this->em->createQueryBuilder() + ->select('COUNT(t.teamid)') ->from(RankCache::class, 'r') ->join('r.team', 't') ->join('t.category', 'tc') - ->select('COUNT(t.teamid)') - ->andWhere('r.contest = :contest') + ->andWhere('r.contest = :contest') ->andWhere('tc.sortorder = :sortorder') - ->andWhere('t.enabled = 1') - ->andWhere(sprintf('r.points_%s > :points OR '. - '(r.points_%s = :points AND r.total%s_%s < :totaltime)', - $variant, $variant, $timeType, $variant)) - ->setParameter('contest', $contest) - ->setParameter('sortorder', $sortOrder) - ->setParameter('points', $points) - ->setParameter('totaltime', $totalTime) + ->andWhere('t.enabled = 1') + ->andWhere(sprintf( + 'r.points_%1$s > :points + OR (r.points_%1$s = :points AND %2$s %3$s :tiebreaker)', + $variant, + $tieField, + $tieOp + )) + ->setParameter('contest', $contest) + ->setParameter('sortorder', $sortOrder) + ->setParameter('points', $points) + ->setParameter('tiebreaker', $tieValue) ->getQuery() ->getSingleScalarResult(); @@ -175,12 +195,15 @@ public function calculateTeamRank( ->andWhere('r.contest = :contest') ->andWhere('tc.sortorder = :sortorder') ->andWhere('t.enabled = 1') - ->andWhere(sprintf('r.points_%s = :points AND r.total%s_%s = :totaltime', - $variant, $timeType, $variant)) + ->andWhere(sprintf( + 'r.points_%1$s = :points AND %2$s = :tiebreaker', + $variant, + $tieField + )) ->setParameter('contest', $contest) ->setParameter('sortorder', $sortOrder) ->setParameter('points', $points) - ->setParameter('totaltime', $totalTime) + ->setParameter('tiebreaker', $tieValue) ->getQuery() ->getResult(); @@ -328,6 +351,10 @@ public function calculateScoreRow( $correctPubl = false; $runtimeJury = PHP_INT_MAX; $runtimePubl = PHP_INT_MAX; + $optmaxJury = null; + $optmaxPubl = null; + $optminJury = null; + $optminPubl = null; foreach ($submissions as $submission) { /** @var Judging|ExternalJudgement|null $judging */ @@ -352,6 +379,27 @@ public function calculateScoreRow( } } + if ($judging !== null && $judging->getResult() == Judging::RESULT_CORRECT) { + $opt = $judging->getSumOptScore(); + if ($opt !== null) { + // Max and Min + if ($optmaxJury === null || $opt > $optmaxJury) { + $optmaxJury = $opt; + } + if ($optminJury === null || $opt < $optminJury) { + $optminJury = $opt; + } + if (!$submission->isAfterFreeze()) { + if ($optmaxPubl === null || $opt > $optmaxPubl) { + $optmaxPubl = $opt; + } + if ($optminPubl === null || $opt < $optminPubl) { + $optminPubl = $opt; + } + } + } + } + // If there is a public and correct submission, we can stop counting // submissions or looking for a correct one (skip steps 2,3) if ($correctPubl) { @@ -483,13 +531,19 @@ public function calculateScoreRow( 'runtimePublic' => $runtimePubl === PHP_INT_MAX ? 0 : $runtimePubl, 'isCorrectPublic' => (int)$correctPubl, 'isFirstToSolve' => (int)$firstToSolve, + 'optScoreMaxRestricted' => $optmaxJury, + 'optScoreMaxPublic' => $optmaxPubl, + 'optScoreMinRestricted' => $optminJury, + 'optScoreMinPublic' => $optminPubl, ]; $this->em->getConnection()->executeQuery('REPLACE INTO scorecache (cid, teamid, probid, submissions_restricted, pending_restricted, solvetime_restricted, runtime_restricted, is_correct_restricted, - submissions_public, pending_public, solvetime_public, runtime_public, is_correct_public, is_first_to_solve) + submissions_public, pending_public, solvetime_public, runtime_public, is_correct_public, is_first_to_solve, + optscore_max_restricted, optscore_max_public, optscore_min_restricted, optscore_min_public) VALUES (:cid, :teamid, :probid, :submissionsRestricted, :pendingRestricted, :solvetimeRestricted, :runtimeRestricted, :isCorrectRestricted, - :submissionsPublic, :pendingPublic, :solvetimePublic, :runtimePublic, :isCorrectPublic, :isFirstToSolve)', $params); + :submissionsPublic, :pendingPublic, :solvetimePublic, :runtimePublic, :isCorrectPublic, :isFirstToSolve, + :optScoreMaxRestricted, :optScoreMaxPublic, :optScoreMinRestricted, :optScoreMinPublic)', $params); if ($this->em->getConnection()->fetchOne('SELECT RELEASE_LOCK(:lock)', ['lock' => $lockString]) != 1) { @@ -545,10 +599,14 @@ public function updateRankCache(Contest $contest, Team $team): void $numPoints = []; $totalTime = []; $totalRuntime = []; + $totalMaxOpt = []; + $totalMinOpt = []; foreach ($variants as $variant => $isRestricted) { $numPoints[$variant] = 0; $totalTime[$variant] = $team->getPenalty(); $totalRuntime[$variant] = 0; + $totalMaxOpt[$variant] = 0; + $totalMinOpt[$variant] = 0; } $penaltyTime = (int) $this->config->get('penalty_time'); @@ -581,6 +639,8 @@ public function updateRankCache(Contest $contest, Team $team): void $scoreIsInSeconds ) + $penalty; $totalRuntime[$variant] += $scoreCache->getRuntime($isRestricted); + $totalMaxOpt[$variant] += $scoreCache->getMaxOptscore($isRestricted); + $totalMinOpt[$variant] += $scoreCache->getMinOptscore($isRestricted); } } } @@ -592,14 +652,30 @@ public function updateRankCache(Contest $contest, Team $team): void 'pointsRestricted' => $numPoints['restricted'], 'totalTimeRestricted' => $totalTime['restricted'], 'totalRuntimeRestricted' => $totalRuntime['restricted'], + 'totalMaxOptRestricted' => $totalMaxOpt['restricted'], + 'totalMinOptRestricted' => $totalMinOpt['restricted'], 'pointsPublic' => $numPoints['public'], 'totalTimePublic' => $totalTime['public'], 'totalRuntimePublic' => $totalRuntime['public'], + 'totalMaxOptPublic' => $totalMaxOpt['public'], + 'totalMinOptPublic' => $totalMinOpt['public'] ]; - $this->em->getConnection()->executeQuery('REPLACE INTO rankcache (cid, teamid, - points_restricted, totaltime_restricted, totalruntime_restricted, - points_public, totaltime_public, totalruntime_public) - VALUES (:cid, :teamid, :pointsRestricted, :totalTimeRestricted, :totalRuntimeRestricted, :pointsPublic, :totalTimePublic, :totalRuntimePublic)', $params); + $this->em->getConnection()->executeQuery( + 'REPLACE INTO rankcache ( + cid, teamid, + points_restricted, totaltime_restricted, totalruntime_restricted, + totaloptscore_max_restricted, totaloptscore_min_restricted, + totaloptscore_max_public, totaloptscore_min_public, + points_public, totaltime_public, totalruntime_public + ) VALUES ( + :cid, :teamid, + :pointsRestricted, :totalTimeRestricted, :totalRuntimeRestricted, + :totalMaxOptRestricted, :totalMinOptRestricted, + :totalMaxOptPublic, :totalMinOptPublic, + :pointsPublic, :totalTimePublic, :totalRuntimePublic + )', + $params + ); if ($this->em->getConnection()->fetchOne('SELECT RELEASE_LOCK(:lock)', ['lock' => $lockString]) != 1) { diff --git a/webapp/src/Twig/TwigExtension.php b/webapp/src/Twig/TwigExtension.php index deff4a4f9c..d1f08e81db 100644 --- a/webapp/src/Twig/TwigExtension.php +++ b/webapp/src/Twig/TwigExtension.php @@ -114,6 +114,7 @@ public function getFilters(): array new TwigFilter('entityIdBadge', $this->entityIdBadge(...), ['is_safe' => ['html']]), new TwigFilter('medalType', $this->awards->medalType(...)), new TwigFilter('numTableActions', $this->numTableActions(...)), + new TwigFilter('formatOptScore', $this->formatOptScore(...)), ]; } @@ -1222,4 +1223,28 @@ protected function numTableActions(array $tableData): int } return $maxNumActions; } + + /** + * Format the given optScore: + * - If it's an integer, display without decimal places + * - Otherwise, display up to the specified number of decimal places + * + * @param float|null $optScore The value to format (nullable) + * @param int $maxDecimals Maximum number of decimal places to show (default = 3) + * @return string Formatted string representation of the score + */ + protected function formatOptScore(?float $optScore, int $maxDecimals = 3): string + { + if ($optScore === null) { + return '-'; + } + + // If the value is an integer, format without decimal places + if ($optScore == floor($optScore)) { + return number_format($optScore, 0, '.', ''); + } + + // Otherwise, format with specified decimal precision + return number_format($optScore, $maxDecimals, '.', ''); + } } diff --git a/webapp/src/Utils/Scoreboard/Scoreboard.php b/webapp/src/Utils/Scoreboard/Scoreboard.php index 6e92330961..d06bb45827 100644 --- a/webapp/src/Utils/Scoreboard/Scoreboard.php +++ b/webapp/src/Utils/Scoreboard/Scoreboard.php @@ -158,7 +158,8 @@ protected function calculateScoreboard(): void $scoreRow->getPending($this->restricted), $scoreRow->getSolveTime($this->restricted), $penalty, - $scoreRow->getRuntime($this->restricted) + $scoreRow->getRuntime($this->restricted), + $scoreRow->getOptscore($this->restricted) ); if ($scoreRow->getIsCorrect($this->restricted)) { @@ -169,6 +170,7 @@ protected function calculateScoreboard(): void $this->scores[$teamId]->solveTimes[] = $solveTime; $this->scores[$teamId]->totalTime += $solveTime + $penalty; $this->scores[$teamId]->totalRuntime += $scoreRow->getRuntime($this->restricted); + $this->scores[$teamId]->totalOptscore += $scoreRow->getOptscore($this->restricted); } } @@ -216,7 +218,7 @@ protected function calculateScoreboard(): void $problemId = $contestProblem->getProbid(); // Provide default scores when nothing submitted for this team + problem yet if (!isset($this->matrix[$teamId][$problemId])) { - $this->matrix[$teamId][$problemId] = new ScoreboardMatrixItem(false, false, 0, 0, 0, 0, 0); + $this->matrix[$teamId][$problemId] = new ScoreboardMatrixItem(false, false, 0, 0, 0, 0, 0, 0); } $problemMatrixItem = $this->matrix[$teamId][$problemId]; @@ -290,6 +292,14 @@ protected function scoreCompare(TeamScore $a, TeamScore $b): int if ($a->totalRuntime != $b->totalRuntime) { return $a->totalRuntime <=> $b->totalRuntime; } + } else if ($this->getOptscoreAsScoreTiebreaker()) { + if ($a->totalRuntime != $b->totalRuntime) { + if ($this->getOptScoreOrder() === "asc") { + return $a->totalOptscore <=> $b->totalOptscore; + } else { + return $b->totalOptscore <=> $a->totalOptscore; + } + } } else { // solvetime ordering if ($a->totalTime != $b->totalTime) { return $a->totalTime <=> $b->totalTime; @@ -458,4 +468,22 @@ public function getRuntimeAsScoreTiebreaker(): bool { return $this->contest->getRuntimeAsScoreTiebreaker(); } + + /** + * Determine whether to order by optscore instead of solvetime + * @return bool + */ + public function getOptScoreAsScoreTiebreaker(): bool + { + return $this->contest->getOptScoreAsScoreTiebreaker(); + } + + /** + * Determine order by optscore + * @return string + */ + public function getOptScoreOrder(): string + { + return $this->contest->getOptScoreOrder(); + } } diff --git a/webapp/src/Utils/Scoreboard/ScoreboardMatrixItem.php b/webapp/src/Utils/Scoreboard/ScoreboardMatrixItem.php index 05b4451884..ec11c86fb6 100644 --- a/webapp/src/Utils/Scoreboard/ScoreboardMatrixItem.php +++ b/webapp/src/Utils/Scoreboard/ScoreboardMatrixItem.php @@ -11,6 +11,7 @@ public function __construct( public int $numSubmissionsPending, public float|string $time, public int $penaltyTime, - public int $runtime + public int $runtime, + public float|null $optscore ) {} } diff --git a/webapp/src/Utils/Scoreboard/SingleTeamScoreboard.php b/webapp/src/Utils/Scoreboard/SingleTeamScoreboard.php index 76aae33bd2..eceb25592d 100644 --- a/webapp/src/Utils/Scoreboard/SingleTeamScoreboard.php +++ b/webapp/src/Utils/Scoreboard/SingleTeamScoreboard.php @@ -46,6 +46,7 @@ protected function calculateScoreboard(): void $teamScore->numPoints += $this->rankCache->getPointsRestricted(); $teamScore->totalTime += $this->rankCache->getTotaltimeRestricted(); $teamScore->totalRuntime += $this->rankCache->getTotalruntimeRestricted(); + $teamScore->totalOptscore += $this->rankCache->getTotalOptscoreRestricted(); } $teamScore->rank = $this->teamRank; @@ -69,7 +70,8 @@ protected function calculateScoreboard(): void $scoreRow->getPending($this->restricted), $scoreRow->getSolveTime($this->restricted), $penalty, - $scoreRow->getRuntime($this->restricted) + $scoreRow->getRuntime($this->restricted), + $scoreRow->getOptscore($this->restricted) ); } @@ -79,7 +81,7 @@ protected function calculateScoreboard(): void $teamId = $this->team->getTeamid(); $problemId = $contestProblem->getProbid(); if (!isset($this->matrix[$teamId][$problemId])) { - $this->matrix[$teamId][$problemId] = new ScoreboardMatrixItem(false, false, 0, 0, 0, 0, 0); + $this->matrix[$teamId][$problemId] = new ScoreboardMatrixItem(false, false, 0, 0, 0, 0, 0, 0); } } } diff --git a/webapp/src/Utils/Scoreboard/TeamScore.php b/webapp/src/Utils/Scoreboard/TeamScore.php index f25805a75c..cbddcf7d8e 100644 --- a/webapp/src/Utils/Scoreboard/TeamScore.php +++ b/webapp/src/Utils/Scoreboard/TeamScore.php @@ -13,6 +13,7 @@ class TeamScore public int $rank = 0; public int $totalTime; public int $totalRuntime = 0; + public ?float $totalOptscore = null; public function __construct(public Team $team) { diff --git a/webapp/templates/jury/partials/contest_form.html.twig b/webapp/templates/jury/partials/contest_form.html.twig index 04ed8018be..1d209fc5ea 100644 --- a/webapp/templates/jury/partials/contest_form.html.twig +++ b/webapp/templates/jury/partials/contest_form.html.twig @@ -20,6 +20,10 @@ {{ form_row(form.allowSubmit) }} {{ form_row(form.processBalloons) }} {{ form_row(form.runtimeAsScoreTiebreaker) }} + {{ form_row(form.optScoreAsScoreTiebreaker) }} +
+ {{ form_row(form.optScoreOrder) }} +
{{ form_row(form.medalsEnabled) }}
{{ form_row(form.medalCategories) }} @@ -157,5 +161,16 @@ $('#contest_medalsEnabled_1, #contest_medalsEnabled_0').on('change', showHideMedals); showHideMedals(); + + function showHideOptScore() { + if ($('#contest_optScoreAsScoreTiebreaker_0').is(':checked')) { + $('[data-opt-score-field]').show(); + } else { + $('[data-opt-score-field]').hide(); + } + } + $('#contest_optScoreAsScoreTiebreaker_0, #contest_optScoreAsScoreTiebreaker_1') + .on('change', showHideOptScore); + showHideOptScore(); }) diff --git a/webapp/templates/jury/partials/submission_graph.html.twig b/webapp/templates/jury/partials/submission_graph.html.twig index d33220e2a9..ca56a5df63 100644 --- a/webapp/templates/jury/partials/submission_graph.html.twig +++ b/webapp/templates/jury/partials/submission_graph.html.twig @@ -6,10 +6,12 @@ testcase CPU times {%- if selectedJudging.result != 'compiler-error' -%} - | max: - {{ selectedJudging.maxRuntime | number_format(3, '.', '') }}s - | sum: {{ selectedJudging.sumRuntime | number_format(3, '.', '') }}s - {% endif %} + | max: {{ selectedJudging.maxRuntime | number_format(3, '.', '') }}s + | sum: {{ selectedJudging.sumRuntime | number_format(3, '.', '') }}s  + {%- if selectedJudging.SumOptScore is not null -%} + | opt score: {{ selectedJudging.SumOptScore | formatOptScore }} + {%- endif %} + {%- endif %}
diff --git a/webapp/templates/partials/scoreboard_table.html.twig b/webapp/templates/partials/scoreboard_table.html.twig index 30ed9f7f8e..ddc2be1148 100644 --- a/webapp/templates/partials/scoreboard_table.html.twig +++ b/webapp/templates/partials/scoreboard_table.html.twig @@ -235,6 +235,8 @@ {{ totalPoints }} {% if scoreboard.getRuntimeAsScoreTiebreaker() %} {{ "%0.3f s" | format(score.totalRuntime/1000.0) }} + {% elseif contest.getOptScoreAsScoreTiebreaker() %} + {{ score.totalOptscore | formatOptScore }} {% else %} {{ totalTime }} {% endif %} @@ -270,7 +272,9 @@ {% set time = '' %} {% if matrixItem.isCorrect %} {% set time = matrixItem.time %} - {% if scoreboard.getRuntimeAsScoreTiebreaker() %} + {% if scoreboard.getOptScoreAsScoreTiebreaker() %} + {% set time = matrixItem.optscore %} + {% elseif scoreboard.getRuntimeAsScoreTiebreaker() %} {% set time = "%0.3f s" | format(matrixItem.runtime / 1000.0) %} {% elseif scoreInSeconds %} {% set time = time | scoreTime | printTimeRelative %} diff --git a/webapp/templates/security/login.html.twig b/webapp/templates/security/login.html.twig index 4a75a3dd69..b8b1b68a17 100644 --- a/webapp/templates/security/login.html.twig +++ b/webapp/templates/security/login.html.twig @@ -118,7 +118,7 @@ Use the form below to change login.' %} -

DOMjudge {{ DOMJUDGE_VERSION }}

+

DOMjudge DSxOOP-8.3.1

diff --git a/webapp/templates/team/partials/submission.html.twig b/webapp/templates/team/partials/submission.html.twig index e3569e9d5b..9c4c9d513a 100644 --- a/webapp/templates/team/partials/submission.html.twig +++ b/webapp/templates/team/partials/submission.html.twig @@ -35,19 +35,29 @@ {% endif %} {% endif %} - - - {% if judging.result != 'compiler-error' %} -
-
- Result: {{ judging.result | printResult }} -
- {% if judging.result == 'correct' and judging.submission.contest.getRuntimeAsScoreTiebreaker() %} + {% if judging.result != 'compiler-error' %} +
- Max. Runtime: {{ "%0.3f s" | format(judging.getMaxRuntime()) }} + Result: {{ judging.result | printResult }}
- {% endif %} -
+ {% if judging.result == 'correct' and judging.submission.contest.getRuntimeAsScoreTiebreaker() %} +
+ Max. Runtime: {{ "%0.3f s" | format(judging.getMaxRuntime()) }} +
+ {% endif %} + {% if judging.result == 'correct' and judging.submission.contest.getOptScoreAsScoreTiebreaker() %} +
+ Opt. Score: {{ judging.SumOptScore | formatOptScore }} +
+ {% endif %} +
+ {% endif %} + + + {% if showTestResults and judging.result != 'compiler-error' %} +
+ Testcase Runs: {{ testcasesruns | displayTestcaseResults(1) }} +
{% endif %} {% if allowDownload %} diff --git a/webapp/templates/team/partials/submission_list.html.twig b/webapp/templates/team/partials/submission_list.html.twig index 77b96645b1..b97c9d043a 100644 --- a/webapp/templates/team/partials/submission_list.html.twig +++ b/webapp/templates/team/partials/submission_list.html.twig @@ -14,6 +14,9 @@ {% if contest.getRuntimeAsScoreTiebreaker() %} runtime {% endif %} + {% if contest.getOptScoreAsScoreTiebreaker() %} + optscore + {% endif %} {% if allowDownload %} {% endif %} @@ -79,6 +82,17 @@ {% endif %} + {% if contest.getOptScoreAsScoreTiebreaker() %} + + + {% if link and submission.getResult()=='correct' %} + {{ submission.judgings.first.SumOptScore | formatOptScore }} + {% else %} + - + {% endif %} + + + {% endif %} {% if allowDownload %}