Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/common/subtask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ interface ParsedSubtask {
score?: number;
id?: number;
if?: number[];
if_score?: number[];
}

export function readSubtasksFromFiles(files: string[], config) {
Expand Down Expand Up @@ -162,6 +163,7 @@ export function normalizeSubtasks(
id: id + 1,
type: 'min',
if: [],
if_score: [],
...s,
score,
time: parseTimeMS(s.time || time, !ignoreParseError) * timeRate,
Expand Down
1 change: 1 addition & 0 deletions packages/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface SubtaskConfig {
memory?: string;
score?: number;
if?: number[];
if_score?: number[];
id?: number;
type?: SubtaskType;
cases?: TestCaseConfig[];
Expand Down
76 changes: 57 additions & 19 deletions packages/hydrojudge/src/flow.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 = {
Expand All @@ -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)
Expand Down Expand Up @@ -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<string, NormalizedSubtask> = {};
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<string, SubtaskResult> = {};
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Ignore non-existent subtasks in if_score dependencies.

If an if_score dependency references a non-existent subtask ID, infos[id] will be undefined, causing scored to evaluate to false and unintentionally fail the subtask. In contrast, the accepted check (lines 100-101) correctly ignores non-existent subtasks (!subtasks[id] || ...).

To maintain consistent behavior and prevent typographical errors in the problem configuration from failing valid subtasks, if_score should also gracefully ignore invalid subtask IDs.

🐛 Proposed fix
-            const scored = (subtask.if_score || []).every((id) => infos[id]?.score > 0);
+            const scored = (subtask.if_score || []).every((id) => !subtasks[id] || infos[id]?.score > 0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const scored = (subtask.if_score || []).every((id) => infos[id]?.score > 0);
const scored = (subtask.if_score || []).every((id) => !subtasks[id] || infos[id]?.score > 0);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/hydrojudge/src/flow.ts` at line 103, Update the if_score dependency
evaluation in the scored expression so non-existent subtask IDs are ignored,
matching the accepted check’s behavior. Guard the infos[id]?.score comparison
with the corresponding subtask existence check, while preserving the requirement
that existing dependencies must have a positive score.

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 });
Expand Down Expand Up @@ -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({
Expand Down
1 change: 1 addition & 0 deletions packages/hydrojudge/src/judge/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const configKey = [
];

const subtasksKey = [
'time', 'memory', 'score', 'if', 'id',
'time', 'memory', 'score', 'if', 'if_score', 'id',
'type', 'cases',
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
Expand All @@ -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);
Expand Down Expand Up @@ -88,13 +95,29 @@ export function SubtaskSettings(props: SubtaskSettingsProps) {
</div>
</Modal>
<Modal opened={depsOpen} onClose={() => setDepsOpen(false)} title={i18n('Set dependencies')}>
<CustomSelectAutoComplete
data={subtaskIds.map((i) => ({ _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
/>
<div style={{ marginBottom: 16 }}>
<Text fw={600} style={{ marginBottom: 4 }}>{i18n('Dependencies')}</Text>
<CustomSelectAutoComplete
data={subtaskIds.map((i) => ({ _id: i.toString(), name: `${i18n('Subtask {0}', i)}` }))}
selectedKeys={parseDependencies(cdeps).map((i) => i.toString())}
onChange={(items) => setDeps(items)}
placeholder={i18n('Dependencies')}
multi
/>
</div>
<div>
<Text fw={600} style={{ marginBottom: 4 }}>{i18n('Positive-score dependencies')}</Text>
<Text c="dimmed" size="sm" style={{ marginBottom: 4 }}>
{i18n('The current subtask is judged only when all selected subtasks have a score greater than 0.')}
</Text>
<CustomSelectAutoComplete
data={subtaskIds.map((i) => ({ _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
/>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 12 }}>
<Button color="blue" onClick={onConfirm}>{i18n('Save')}</Button>
</div>
Expand All @@ -111,6 +134,10 @@ export function SubtaskSettings(props: SubtaskSettingsProps) {
<Text><i className="icon icon-diagram-tree" /></Text>
<Text>{i18n('Dependencies')}: {deps.length ? deps.join(', ') : i18n('(None)')}</Text>
</div>
<div style={{ paddingLeft: 22, display: 'flex', alignItems: 'center', cursor: 'pointer', gap: 8 }} onClick={() => setDepsOpen(true)}>
<Text><i className="icon icon-diagram-tree" /></Text>
<Text>{i18n('Positive-score dependencies')}: {scoreDeps.length ? scoreDeps.join(', ') : i18n('(None)')}</Text>
</div>
<div style={{ paddingLeft: 22, display: 'flex', alignItems: 'center', gap: 8 }}>
<i className="icon icon-asterisk" />
<Text>{i18n('Scoring method')}</Text>
Expand Down
2 changes: 2 additions & 0 deletions packages/ui-default/locales/zh.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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...: 准备上传...
Expand Down Expand Up @@ -950,6 +951,7 @@ Text: 文本
The 'default' role applies to ALL REGISTERED USER.: default 角色作用于<b>所有已注册用户</b>
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 个字符。
Expand Down
Loading