diff --git a/packages/common/subtask.ts b/packages/common/subtask.ts index 6c8f19238b..a840a138e8 100644 --- a/packages/common/subtask.ts +++ b/packages/common/subtask.ts @@ -90,6 +90,7 @@ interface ParsedSubtask { score?: number; id?: number; if?: number[]; + if_score?: number[]; } export function readSubtasksFromFiles(files: string[], config) { @@ -162,6 +163,7 @@ export function normalizeSubtasks( id: id + 1, type: 'min', if: [], + if_score: [], ...s, score, time: parseTimeMS(s.time || time, !ignoreParseError) * timeRate, diff --git a/packages/common/types.ts b/packages/common/types.ts index d61c7e0a90..279347341c 100644 --- a/packages/common/types.ts +++ b/packages/common/types.ts @@ -31,6 +31,7 @@ export interface SubtaskConfig { memory?: string; score?: number; if?: number[]; + if_score?: number[]; id?: number; type?: SubtaskType; cases?: TestCaseConfig[]; diff --git a/packages/hydrojudge/src/flow.ts b/packages/hydrojudge/src/flow.ts index 71a74e0b3a..1bf8038314 100644 --- a/packages/hydrojudge/src/flow.ts +++ b/packages/hydrojudge/src/flow.ts @@ -1,6 +1,6 @@ import Queue from 'p-queue'; import { - JudgeResultBody, NormalizedCase, NormalizedSubtask, STATUS, + JudgeResultBody, NormalizedCase, NormalizedSubtask, STATUS, type SubtaskResult, type SubtaskType, } from '@hydrooj/common'; import { getConfig } from './config'; import { FormatError } from './error'; @@ -19,7 +19,7 @@ const Score = { min: Math.min, }; -function judgeSubtask(subtask: NormalizedSubtask, sid: string, judgeCase: Task['judgeCase']) { +function judgeSubtask(subtask: NormalizedSubtask, sid: string, judgeCase: Task['judgeCase'], skip = false) { return async (ctx: Context) => { subtask.type ||= 'min'; const ctxSubtask = { @@ -33,7 +33,8 @@ function judgeSubtask(subtask: NormalizedSubtask, sid: string, judgeCase: Task[' for (const cid in subtask.cases) { const runner = judgeCase(subtask.cases[cid]); cases.push(ctx.queue.add(async () => { - const res = (ctx.errored + const res = (skip + || ctx.errored || (subtask.type === 'min' && ctxSubtask.score === 0) || (subtask.type === 'max' && ctxSubtask.score === subtask.score) || (subtask.if || []).filter((i) => ctx.failed[i]).length) @@ -71,13 +72,47 @@ function judgeSubtask(subtask: NormalizedSubtask, sid: string, judgeCase: Task[' } ctx.total_status = Math.max(ctx.total_status, ctxSubtask.status); return { - type: ctxSubtask.subtask.type, + type: ctxSubtask.subtask.type as SubtaskType, score: ctxSubtask.score, status: ctxSubtask.status, }; }; } +async function judgeScoreDependentSubtasks(ctx: Context, task: Task) { + const subtasks: Record = {}; + for (const [key, value] of Object.entries(ctx.config.subtasks)) { + subtasks[value.id?.toString() || key] = value; + } + const pending = new Set(Object.keys(subtasks)); + const infos: Record = {}; + while (pending.size) { + const ready = [...pending].filter((sid) => { + const subtask = subtasks[sid]; + return [...(subtask.if || []), ...(subtask.if_score || [])] + .every((id) => !subtasks[id] || !pending.has(id.toString())); + }); + if (!ready.length) throw new FormatError('Circular dependency between subtasks.'); + for (const sid of ready) pending.delete(sid); + // eslint-disable-next-line no-await-in-loop + await Promise.all(ready.map(async (sid) => { + const subtask = subtasks[sid]; + const accepted = (subtask.if || []).every((id) => ( + !subtasks[id] || (infos[id] && infos[id].status <= STATUS.STATUS_ACCEPTED) + )); + const scored = (subtask.if_score || []).every((id) => infos[id]?.score > 0); + if (!accepted || !scored) { + ctx.failed[sid] = true; + await judgeSubtask(subtask, sid, task.judgeCase, true)(ctx); + return; + } + infos[sid] = await judgeSubtask(subtask, sid, task.judgeCase)(ctx); + })); + } + for (const info of Object.values(infos)) ctx.total_score += info.score; + return infos; +} + export const runFlow = async (ctx: Context, task: Task) => { if (!ctx.config.subtasks.length) throw new FormatError('Problem data not found.'); ctx.next({ status: STATUS.STATUS_COMPILING }); @@ -111,21 +146,24 @@ export const runFlow = async (ctx: Context, task: Task) => { ctx.end({ nop: true }); } } else { - const infos = {}; - await Promise.all(Object.entries(ctx.config.subtasks).map(async ([key, value]) => { - const sid = value.id?.toString() || key; - infos[sid] = await judgeSubtask(value, sid, task.judgeCase)(ctx); - })); - for (const [key, value] of Object.entries(ctx.config.subtasks)) { - let effective = true; - const sid = value.id?.toString() || key; - for (const required of value.if || []) { - if (ctx.failed[required.toString()]) effective = false; - } - if (effective) ctx.total_score += infos[sid].score; - else { - ctx.failed[sid] = true; - delete infos[sid]; + const hasScoreDependencies = ctx.config.subtasks.some((i) => i.if_score?.length); + const infos = hasScoreDependencies ? await judgeScoreDependentSubtasks(ctx, task) : {}; + if (!hasScoreDependencies) { + await Promise.all(Object.entries(ctx.config.subtasks).map(async ([key, value]) => { + const sid = value.id?.toString() || key; + infos[sid] = await judgeSubtask(value, sid, task.judgeCase)(ctx); + })); + for (const [key, value] of Object.entries(ctx.config.subtasks)) { + let effective = true; + const sid = value.id?.toString() || key; + for (const required of value.if || []) { + if (ctx.failed[required.toString()]) effective = false; + } + if (effective) ctx.total_score += infos[sid].score; + else { + ctx.failed[sid] = true; + delete infos[sid]; + } } } ctx.end({ diff --git a/packages/hydrojudge/src/judge/run.ts b/packages/hydrojudge/src/judge/run.ts index 87b9b71d5e..0dc9fea81f 100644 --- a/packages/hydrojudge/src/judge/run.ts +++ b/packages/hydrojudge/src/judge/run.ts @@ -60,6 +60,7 @@ export const judge = async (ctx: Context) => { time: ctx.config.time, memory: ctx.config.memory, if: [], + if_score: [], cases: ctx.input.map((i, idx) => ({ id: idx + 1, time: ctx.config.time, diff --git a/packages/ui-default/components/monaco/schema/problemconfig.ts b/packages/ui-default/components/monaco/schema/problemconfig.ts index 875551e442..b8787a7de1 100644 --- a/packages/ui-default/components/monaco/schema/problemconfig.ts +++ b/packages/ui-default/components/monaco/schema/problemconfig.ts @@ -32,6 +32,7 @@ const problemConfigSchema: JSONSchema7 = { score: { $ref: '#/definitions/score', description: 'score' }, cases: { $ref: '#/definitions/cases' }, if: { type: 'array', items: { type: 'integer' } }, + if_score: { type: 'array', items: { type: 'integer' } }, id: { type: 'integer' }, }, required: ['score'], diff --git a/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx b/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx index ca86723580..d7f9a0d9d2 100644 --- a/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx +++ b/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx @@ -35,7 +35,7 @@ const configKey = [ ]; const subtasksKey = [ - 'time', 'memory', 'score', 'if', 'id', + 'time', 'memory', 'score', 'if', 'if_score', 'id', 'type', 'cases', ]; diff --git a/packages/ui-default/components/problemconfig/reducer/config.ts b/packages/ui-default/components/problemconfig/reducer/config.ts index b3efc9c73f..ebb7f3d7f9 100644 --- a/packages/ui-default/components/problemconfig/reducer/config.ts +++ b/packages/ui-default/components/problemconfig/reducer/config.ts @@ -127,6 +127,10 @@ export default function reducer(state = { if (action.payload.memory) subtask.memory = action.payload.memory; if (action.payload.score) subtask.score = +action.payload.score || 0; if (action.payload.if) subtask.if = action.payload.if; + if ('if_score' in action.payload) { + if (action.payload.if_score?.length) subtask.if_score = action.payload.if_score; + else delete subtask.if_score; + } if (action.payload.type) subtask.type = action.payload.type; if (!subtask.time) delete subtask.time; if (!subtask.memory) delete subtask.memory; diff --git a/packages/ui-default/components/problemconfig/tree/SubtaskSettings.tsx b/packages/ui-default/components/problemconfig/tree/SubtaskSettings.tsx index db132d60d1..6948992c00 100644 --- a/packages/ui-default/components/problemconfig/tree/SubtaskSettings.tsx +++ b/packages/ui-default/components/problemconfig/tree/SubtaskSettings.tsx @@ -14,6 +14,10 @@ interface SubtaskSettingsProps { memory: string; } +function parseDependencies(value: string) { + return value.split(',').map((i) => i.trim()).filter((i) => +i).map((i) => +i); +} + export function SubtaskSettings(props: SubtaskSettingsProps) { const [open, setOpen] = React.useState(false); const [depsOpen, setDepsOpen] = React.useState(false); @@ -22,12 +26,14 @@ export function SubtaskSettings(props: SubtaskSettingsProps) { const time = useSelector((state: RootState) => state.config.subtasks.find((i) => i.id === props.subtaskId).time); const memory = useSelector((state: RootState) => state.config.subtasks.find((i) => i.id === props.subtaskId).memory); const deps = useSelector((state: RootState) => state.config.subtasks.find((i) => i.id === props.subtaskId).if || [], isEqual); + const scoreDeps = useSelector((state: RootState) => state.config.subtasks.find((i) => i.id === props.subtaskId).if_score || [], isEqual); const type = useSelector((state: RootState) => state.config.subtasks.find((i) => i.id === props.subtaskId).type || 'min'); const [ctime, setTime] = React.useState(time); const [cmemory, setMemory] = React.useState(memory); const [cscore, setScore] = React.useState(score); const [cdeps, setDeps] = React.useState(deps.join(', ')); + const [cscoreDeps, setScoreDeps] = React.useState(scoreDeps.join(', ')); const [ctype, setType] = React.useState(type); const dispatch = useDispatch(); @@ -49,7 +55,8 @@ export function SubtaskSettings(props: SubtaskSettingsProps) { time: ctime, memory: cmemory, score: cscore, - if: cdeps.split(',').map((i) => i.trim()).filter((i) => +i).map((i) => +i), + if: parseDependencies(cdeps), + if_score: parseDependencies(cscoreDeps), }, }); setOpen(false); @@ -88,13 +95,29 @@ export function SubtaskSettings(props: SubtaskSettingsProps) { setDepsOpen(false)} title={i18n('Set dependencies')}> - ({ _id: i, name: `${i18n('Subtask {0}', i)}` }))} - setSelectItems={cdeps.split(',').map((i) => i.trim()).filter((i) => +i).map((i) => +i)} - onChange={(items) => setDeps(items)} - placeholder="dependencies" - multi - /> +
+ {i18n('Dependencies')} + ({ _id: i.toString(), name: `${i18n('Subtask {0}', i)}` }))} + selectedKeys={parseDependencies(cdeps).map((i) => i.toString())} + onChange={(items) => setDeps(items)} + placeholder={i18n('Dependencies')} + multi + /> +
+
+ {i18n('Positive-score dependencies')} + + {i18n('The current subtask is judged only when all selected subtasks have a score greater than 0.')} + + ({ _id: i.toString(), name: `${i18n('Subtask {0}', i)}` }))} + selectedKeys={parseDependencies(cscoreDeps).map((i) => i.toString())} + onChange={(items) => setScoreDeps(items)} + placeholder={i18n('Positive-score dependencies')} + multi + /> +
@@ -111,6 +134,10 @@ export function SubtaskSettings(props: SubtaskSettingsProps) { {i18n('Dependencies')}: {deps.length ? deps.join(', ') : i18n('(None)')} +
setDepsOpen(true)}> + + {i18n('Positive-score dependencies')}: {scoreDeps.length ? scoreDeps.join(', ') : i18n('(None)')} +
{i18n('Scoring method')} diff --git a/packages/ui-default/locales/zh.yaml b/packages/ui-default/locales/zh.yaml index 153729051a..edb7e3762d 100644 --- a/packages/ui-default/locales/zh.yaml +++ b/packages/ui-default/locales/zh.yaml @@ -700,6 +700,7 @@ Please select at least one user to perform this operation.: 请选择至少一 Please set the balloon color for each problem first.: 请先为每道题设置气球颜色。 Please wait until contest host unfreeze the scoreboard.: 请等待比赛主办方解除封榜。 Polyhedron supports managing problem version history, testing solutions, checking time limits, composing contest statements, cooperation and much more.: Polyhedron 支持题目版本管理,代码测试,时限检验,制作比赛题面,多人协作等等功能。 +Positive-score dependencies: 正分依赖 Preference Settings: 偏好设置 preferredPrefix_hint: 此选项用于重排题号。例如,若题目包中所给的题号分别是 P1001, P1002, P1003,而此选项填写了 T,则导入后三道题的题号分别为 T1001, T1002 和 T1003。 Preparing Upload...: 准备上传... @@ -950,6 +951,7 @@ Text: 文本 The 'default' role applies to ALL REGISTERED USER.: default 角色作用于所有已注册用户 The contest is a flexible time contest. You need to complete the contest within a specified time after you attended.: 本场比赛采用灵活时间模式,你需要在参加后的指定时间内完成比赛。 The contest is ended. New submissions will be treated as correction submissions and will not be counted in the contest.: 比赛已经结束。新提交将被视为补题提交,不计入比赛成绩。 +The current subtask is judged only when all selected subtasks have a score greater than 0.: 仅当所有选中的子任务得分均大于 0 时,才评测当前子任务。 The group to join when user joining the domain.: 加入域时自动加入的小组。 The homework's deadline is due but in extension. You can still submit for this problem but your score will be penalized.: 作业已超过截止时间,但仍在延期时间内。您递交题目将无法获得全部分数。 The invitation code to enter to successfully join the domain. You can only use letters and numbers in the code and it should not be longer than 64 characters.: 加入此域的邀请码。您只能使用字母和数字,并且不能长于 64 个字符。