From a25af55c2f8c85d330fee9a00b079e375b4ca429 Mon Sep 17 00:00:00 2001 From: zhou-jk Date: Sun, 15 Mar 2026 16:08:57 +0800 Subject: [PATCH] feat: support configurable problem total score --- config-example.yaml | 1 + src/config/config.schema.ts | 4 +++ src/problem-type/common/meta-and-subtasks.ts | 36 +++++++++++++------ .../problem-judge-info.interface.ts | 5 +++ .../types/interaction/problem-type.service.ts | 4 ++- .../problem-judge-info.interface.ts | 5 +++ .../submit-answer/problem-type.service.ts | 5 +-- .../problem-judge-info.interface.ts | 5 +++ .../types/traditional/problem-type.service.ts | 4 ++- src/submission/submission.service.ts | 4 ++- 10 files changed, 57 insertions(+), 16 deletions(-) diff --git a/config-example.yaml b/config-example.yaml index 40cb95e..f1d0cce 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -99,6 +99,7 @@ resourceLimit: problemTimeLimit: 2000 problemMemoryLimit: 512 submissionFileSize: 10485760 + maxTotalScore: 1000000 queryLimit: problemSet: 100 submissions: 10 diff --git a/src/config/config.schema.ts b/src/config/config.schema.ts index 09ce8ac..fa0a1cf 100644 --- a/src/config/config.schema.ts +++ b/src/config/config.schema.ts @@ -448,6 +448,10 @@ class ResourceLimitConfig { @IsInt() @Min(0) readonly submissionFileSize: number; + + @IsInt() + @Min(1) + readonly maxTotalScore: number; } class QueryLimitConfig { diff --git a/src/problem-type/common/meta-and-subtasks.ts b/src/problem-type/common/meta-and-subtasks.ts index bba9362..d35e147 100644 --- a/src/problem-type/common/meta-and-subtasks.ts +++ b/src/problem-type/common/meta-and-subtasks.ts @@ -10,6 +10,7 @@ import { restrictProperties } from "./restrict-properties"; interface JudgeInfoWithMetaAndSubtasks { timeLimit?: number; memoryLimit?: number; + totalScore?: number; fileIo?: { inputFilename: string; @@ -48,6 +49,8 @@ interface ValidateMetaAndSubtasksOptions { enableInputFile: boolean | "optional"; enableOutputFile: boolean | "optional"; enableUserOutputFilename: boolean; + + maxTotalScore?: number; } export function validateMetaAndSubtasks( @@ -86,6 +89,13 @@ export function validateMetaAndSubtasks( validateMemoryLimit(judgeInfo.memoryLimit, "TASK"); } + if (judgeInfo.totalScore != null) { + if (!Number.isSafeInteger(judgeInfo.totalScore) || judgeInfo.totalScore <= 0) + throw ["INVALID_TOTAL_SCORE", judgeInfo.totalScore]; + if (options.maxTotalScore != null && judgeInfo.totalScore > options.maxTotalScore) + throw ["TOTAL_SCORE_TOO_LARGE", judgeInfo.totalScore, options.maxTotalScore]; + } + if (options.enableFileIo && judgeInfo.fileIo) { if (typeof judgeInfo.fileIo.inputFilename !== "string" || !isValidFilename(judgeInfo.fileIo.inputFilename)) throw ["INVALID_FILEIO_FILENAME", judgeInfo.fileIo.inputFilename]; @@ -114,7 +124,7 @@ export function validateMetaAndSubtasks( throw ["INVALID_SCORING_TYPE", i + 1, scoringType]; } - if (points != null && (typeof points !== "number" || points < 0 || points > 100)) + if (points != null && (typeof points !== "number" || points < 0)) throw ["INVALID_POINTS_SUBTASK", i + 1, points]; if (Array.isArray(dependencies)) { @@ -185,7 +195,7 @@ export function validateMetaAndSubtasks( delete testcase.memoryLimit; } - if (points != null && (typeof points !== "number" || points < 0 || points > 100)) + if (points != null && (typeof points !== "number" || points < 0)) throw ["INVALID_POINTS_TESTCASE", i + 1, j + 1, points]; restrictProperties(testcase, [ @@ -198,16 +208,7 @@ export function validateMetaAndSubtasks( ]); }); - // eslint-disable-next-line @typescript-eslint/no-shadow - const sum = testcases.reduce((s, { points }) => (points ? s + points : s), 0); - if (sum > 100) { - throw ["POINTS_SUM_UP_TO_LARGER_THAN_100_TESTCASES", i + 1, sum]; - } }); - const sum = (judgeInfo.subtasks || []).reduce((s, { points }) => (points ? s + points : s), 0); - if (sum > 100) { - throw ["POINTS_SUM_UP_TO_LARGER_THAN_100_SUBTASKS", sum]; - } try { toposort.array( @@ -224,4 +225,17 @@ export function validateMetaAndSubtasks( judgeInfo.subtasks.reduce((count, subtask) => count + subtask.testcases.length, 0) > options.testcaseLimit ) throw ["TOO_MANY_TESTCASES"]; + + // Validate subtask points sum + const totalScore = judgeInfo.totalScore || 100; + const subtasksWithPoints = judgeInfo.subtasks?.filter(s => s.points != null) || []; + if (subtasksWithPoints.length > 0) { + const pointsSum = subtasksWithPoints.reduce((sum, s) => sum + (s.points || 0), 0); + if (pointsSum > totalScore) { + throw ["SUBTASK_POINTS_SUM_TOO_LARGE", pointsSum, totalScore]; + } + if (judgeInfo.subtasks && subtasksWithPoints.length === judgeInfo.subtasks.length && pointsSum !== totalScore) { + throw ["SUBTASK_POINTS_SUM_NOT_EQUAL", pointsSum, totalScore]; + } + } } diff --git a/src/problem-type/types/interaction/problem-judge-info.interface.ts b/src/problem-type/types/interaction/problem-judge-info.interface.ts index e0f09e0..d223b0c 100644 --- a/src/problem-type/types/interaction/problem-judge-info.interface.ts +++ b/src/problem-type/types/interaction/problem-judge-info.interface.ts @@ -9,6 +9,11 @@ export interface ProblemJudgeInfoInteraction extends ProblemJudgeInfo { timeLimit: number; memoryLimit: number; + /* + * Total score of the problem (default: 100) + */ + totalScore?: number; + /* * If ture, samples in statement will be run before all subtasks * If a submission failed on samples, all subtasks will be skipped diff --git a/src/problem-type/types/interaction/problem-type.service.ts b/src/problem-type/types/interaction/problem-type.service.ts index 5c8815d..2ad3aaf 100644 --- a/src/problem-type/types/interaction/problem-type.service.ts +++ b/src/problem-type/types/interaction/problem-type.service.ts @@ -77,7 +77,8 @@ export class ProblemTypeInteractionService enableUserOutputFilename: false, hardTimeLimit, hardMemoryLimit, - testcaseLimit: ignoreLimits ? null : this.configService.config.resourceLimit.problemTestcases + testcaseLimit: ignoreLimits ? null : this.configService.config.resourceLimit.problemTestcases, + maxTotalScore: this.configService.config.resourceLimit.maxTotalScore }); const { interactor } = judgeInfo; @@ -125,6 +126,7 @@ export class ProblemTypeInteractionService restrictProperties(judgeInfo, [ "timeLimit", "memoryLimit", + "totalScore", "runSamples", "subtasks", "interactor", diff --git a/src/problem-type/types/submit-answer/problem-judge-info.interface.ts b/src/problem-type/types/submit-answer/problem-judge-info.interface.ts index d74156e..a7d2d4d 100644 --- a/src/problem-type/types/submit-answer/problem-judge-info.interface.ts +++ b/src/problem-type/types/submit-answer/problem-judge-info.interface.ts @@ -2,6 +2,11 @@ import { ProblemJudgeInfo } from "@/problem/problem-judge-info.interface"; import { Checker } from "@/problem-type/common/checker"; export interface ProblemJudgeInfoSubmitAnswer extends ProblemJudgeInfo { + /* + * Total score of the problem (default: 100) + */ + totalScore?: number; + /* * There could be multiple subtasks in a problem * Each subtask contains some testcases diff --git a/src/problem-type/types/submit-answer/problem-type.service.ts b/src/problem-type/types/submit-answer/problem-type.service.ts index afe8b18..f431194 100644 --- a/src/problem-type/types/submit-answer/problem-type.service.ts +++ b/src/problem-type/types/submit-answer/problem-type.service.ts @@ -69,7 +69,8 @@ export class ProblemTypeSubmitAnswerService enableFileIo: false, enableInputFile: "optional", enableOutputFile: true, - enableUserOutputFilename: true + enableUserOutputFilename: true, + maxTotalScore: this.configService.config.resourceLimit.maxTotalScore }); validateChecker(judgeInfo, testData, { @@ -79,7 +80,7 @@ export class ProblemTypeSubmitAnswerService hardMemoryLimit: ignoreLimits ? null : this.configService.config.resourceLimit.problemMemoryLimit }); - restrictProperties(judgeInfo, ["subtasks", "checker"]); + restrictProperties(judgeInfo, ["totalScore", "subtasks", "checker"]); } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/problem-type/types/traditional/problem-judge-info.interface.ts b/src/problem-type/types/traditional/problem-judge-info.interface.ts index b12849a..58ba3eb 100644 --- a/src/problem-type/types/traditional/problem-judge-info.interface.ts +++ b/src/problem-type/types/traditional/problem-judge-info.interface.ts @@ -10,6 +10,11 @@ export interface ProblemJudgeInfoTraditional extends ProblemJudgeInfo { timeLimit: number; memoryLimit: number; + /* + * Total score of the problem (default: 100) + */ + totalScore?: number; + /* * Be null if not using file IO */ diff --git a/src/problem-type/types/traditional/problem-type.service.ts b/src/problem-type/types/traditional/problem-type.service.ts index e91827e..5642659 100644 --- a/src/problem-type/types/traditional/problem-type.service.ts +++ b/src/problem-type/types/traditional/problem-type.service.ts @@ -76,7 +76,8 @@ export class ProblemTypeTraditionalService enableUserOutputFilename: false, hardTimeLimit: ignoreLimits ? null : this.configService.config.resourceLimit.problemTimeLimit, hardMemoryLimit: ignoreLimits ? null : this.configService.config.resourceLimit.problemMemoryLimit, - testcaseLimit: ignoreLimits ? null : this.configService.config.resourceLimit.problemTestcases + testcaseLimit: ignoreLimits ? null : this.configService.config.resourceLimit.problemTestcases, + maxTotalScore: this.configService.config.resourceLimit.maxTotalScore }); validateChecker(judgeInfo, testData, { @@ -91,6 +92,7 @@ export class ProblemTypeTraditionalService restrictProperties(judgeInfo, [ "timeLimit", "memoryLimit", + "totalScore", "fileIo", "runSamples", "subtasks", diff --git a/src/submission/submission.service.ts b/src/submission/submission.service.ts index bf0958d..b3fd176 100644 --- a/src/submission/submission.service.ts +++ b/src/submission/submission.service.ts @@ -63,6 +63,7 @@ interface SubmissionTaskExtraInfo extends JudgeTaskExtraInfo { uuid: string; url: string; }; + problemTotalScore: number; } function makeSubmissionPriority( @@ -770,7 +771,8 @@ export class SubmissionService implements JudgeTaskService