mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
judge: change objective config format
This commit is contained in:
1 parent
e509bde41b
commit
76745639c1
7 files changed
+118
-66
No files matched your search
@@ -88,7 +88,7 @@ export default async function readCases(folder: string, cfg: Record<string, any>
|
||||
const checkFile = ensureFile(folder);
|
||||
const result = await readYamlCases(config, checkFile)
|
||||
.catch((e) => { throw changeErrorType(e, FormatError); });
|
||||
result.count = result.outputs?.length || Math.sum((result.subtasks || []).map((s) => s.cases.length));
|
||||
result.count = Object.keys(result.answers || {}).length || Math.sum((result.subtasks || []).map((s) => s.cases.length));
|
||||
if (!result.count) {
|
||||
try {
|
||||
result.subtasks = readSubtasksFromFiles(await collectFiles(folder), cfg);
|
||||
|
||||
@@ -62,10 +62,7 @@ export interface Config {
|
||||
hack?: string;
|
||||
interactor?: string;
|
||||
|
||||
outputs?: {
|
||||
output: string,
|
||||
score: number,
|
||||
}[];
|
||||
answers?: Record<string, [answer: string | string[], score: number]>;
|
||||
}
|
||||
|
||||
export interface ContextSubTask {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import assert from 'assert';
|
||||
import { readFile } from 'fs-extra';
|
||||
import yaml from 'js-yaml';
|
||||
import { STATUS } from '@hydrooj/utils/lib/status';
|
||||
import { Context } from './interface';
|
||||
|
||||
@@ -11,27 +13,98 @@ export async function judge({
|
||||
: ('content' in code)
|
||||
? code.content.toString().replace(/\r/g, '')
|
||||
: '';
|
||||
const outputs = answer.split('\n');
|
||||
let answers;
|
||||
try {
|
||||
answers = yaml.load(answer);
|
||||
assert(typeof answers === 'object');
|
||||
} catch (e) {
|
||||
return end({
|
||||
status: STATUS.STATUS_WRONG_ANSWER,
|
||||
score: 0,
|
||||
message: 'Unable to parse answer.',
|
||||
time: 0,
|
||||
memory: 0,
|
||||
});
|
||||
}
|
||||
let totalScore = 0;
|
||||
let totalStatus = 0;
|
||||
for (const i in config.outputs) {
|
||||
const c = config.outputs[i];
|
||||
outputs[i] = outputs[i] || '';
|
||||
let status = STATUS.STATUS_WRONG_ANSWER;
|
||||
let score = 0;
|
||||
if (outputs[i].trim() === (c.output || c[0]).trim()) {
|
||||
score = c.score || c[1];
|
||||
status = STATUS.STATUS_ACCEPTED;
|
||||
}
|
||||
for (const key in config.answers) {
|
||||
const [subtaskId, caseId] = key.split('-').map(Number);
|
||||
const ansInfo = config.answers[key] as [string | string[], number];
|
||||
const score = ansInfo[1];
|
||||
const baseInfo = {
|
||||
subtaskId,
|
||||
id: caseId,
|
||||
time: 0,
|
||||
memory: 0,
|
||||
};
|
||||
if (typeof ansInfo[0] === 'string') {
|
||||
if (answer[0].trim() === answers[key].trim()) {
|
||||
totalScore += score;
|
||||
totalStatus = Math.max(status, totalStatus);
|
||||
totalStatus = Math.max(totalStatus, STATUS.STATUS_ACCEPTED);
|
||||
next({
|
||||
status: totalStatus,
|
||||
progress: (100 * (+i + 1)) / config.outputs.length,
|
||||
case: {
|
||||
status, score, time: 0, memory: 0, message: '',
|
||||
...baseInfo,
|
||||
status: STATUS.STATUS_ACCEPTED,
|
||||
score,
|
||||
message: 'Correct',
|
||||
},
|
||||
}, +i + 1);
|
||||
}, +caseId);
|
||||
} else {
|
||||
totalStatus = STATUS.STATUS_WRONG_ANSWER;
|
||||
next({
|
||||
status: totalStatus,
|
||||
case: {
|
||||
...baseInfo,
|
||||
status: STATUS.STATUS_WRONG_ANSWER,
|
||||
score: 0,
|
||||
message: 'Incorrect',
|
||||
},
|
||||
}, +caseId);
|
||||
}
|
||||
} else {
|
||||
const stdAns = new Set(ansInfo[0]);
|
||||
const ans = new Set(answers[key]);
|
||||
const correct = stdAns.size === ans.size && [...stdAns].every((x) => ans.has(x));
|
||||
const partialCorrect = [...stdAns].some((x) => ans.has(x)) && [...ans].every((x) => stdAns.has(x));
|
||||
if (correct) {
|
||||
totalScore += score;
|
||||
totalStatus = Math.max(totalStatus, STATUS.STATUS_ACCEPTED);
|
||||
next({
|
||||
status: totalStatus,
|
||||
case: {
|
||||
...baseInfo,
|
||||
status: STATUS.STATUS_ACCEPTED,
|
||||
score,
|
||||
message: 'Correct',
|
||||
},
|
||||
}, +caseId);
|
||||
} else if (partialCorrect) {
|
||||
totalScore += Math.floor(score / 2);
|
||||
totalStatus = STATUS.STATUS_WRONG_ANSWER;
|
||||
next({
|
||||
status: totalStatus,
|
||||
case: {
|
||||
...baseInfo,
|
||||
status: STATUS.STATUS_WRONG_ANSWER,
|
||||
score: Math.floor(score / 2),
|
||||
message: 'Partially Correct',
|
||||
},
|
||||
}, +caseId);
|
||||
} else {
|
||||
totalStatus = STATUS.STATUS_WRONG_ANSWER;
|
||||
next({
|
||||
status: totalStatus,
|
||||
case: {
|
||||
...baseInfo,
|
||||
status: STATUS.STATUS_WRONG_ANSWER,
|
||||
score: 0,
|
||||
message: 'Incorrect',
|
||||
},
|
||||
}, +caseId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return end({
|
||||
status: totalStatus, score: totalScore, time: 0, memory: 0,
|
||||
|
||||
@@ -170,7 +170,7 @@ export interface ProblemConfigFile {
|
||||
user_extra_files?: string[];
|
||||
judge_extra_files?: string[];
|
||||
detail?: boolean;
|
||||
outputs?: [string, number][];
|
||||
answers?: Record<string, [string | string[], number]>;
|
||||
redirect?: string;
|
||||
cases?: TestCaseConfig[];
|
||||
subtasks?: SubtaskConfig[];
|
||||
|
||||
@@ -70,9 +70,16 @@ const problemConfigSchema = {
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
outputs: {
|
||||
answers: {
|
||||
type: 'object',
|
||||
patternProperties: {
|
||||
'^\\d+(-\\d+)?$': {
|
||||
type: 'array',
|
||||
minLength: 1,
|
||||
minLength: 2,
|
||||
maxLength: 2,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import yaml from 'js-yaml';
|
||||
import ReactDOM, { createRoot } from 'react-dom/client';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import React from 'react';
|
||||
import { getScoreColor } from '@hydrooj/utils/lib/status';
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
@@ -174,39 +174,10 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
|
||||
}
|
||||
|
||||
async function loadObjective() {
|
||||
try {
|
||||
let s = '';
|
||||
try {
|
||||
s = JSON.parse(UiContext.pdoc.content);
|
||||
} catch {
|
||||
s = UiContext.pdoc.content;
|
||||
}
|
||||
if (typeof s === 'object') {
|
||||
const langs = Object.keys(s);
|
||||
const f = langs.filter((i) => i.startsWith(UserContext.viewLang));
|
||||
if (s[UserContext.viewLang]) s = s[UserContext.viewLang];
|
||||
else if (f.length) s = s[f[0]];
|
||||
else s = s[langs[0]];
|
||||
}
|
||||
const props = yaml.load(s);
|
||||
if (!(props instanceof Array)) return;
|
||||
$('.outer-loader-container').show();
|
||||
const { default: Objective } = await import('vj/components/objective-question/index');
|
||||
|
||||
ReactDOM.createRoot($('.problem-content').get(0)).render(
|
||||
<div className="section__body typo">
|
||||
<Objective panel={props} target={UiContext.postSubmitUrl}></Objective>
|
||||
</div>,
|
||||
);
|
||||
$('.outer-loader-container').hide();
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
async function loadObjectiveNew() {
|
||||
$('.outer-loader-container').show();
|
||||
const ans = {};
|
||||
let objNum = 0;
|
||||
const reg = /{{ (input|select|multiselect)\([a-zA-Z0-9]+\) }}/g;
|
||||
const reg = /{{ (input|select|multiselect)\(\d+(-\d+)?\) }}/g;
|
||||
$('.problem-content .typo').children().each((i, e) => {
|
||||
if (e.tagName === 'PRE' && !e.children[0].className.includes('#input')) return;
|
||||
const objQuestions = [];
|
||||
@@ -218,29 +189,32 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
|
||||
objQuestions.forEach((obj) => {
|
||||
objNum++;
|
||||
const [objData, objType] = obj;
|
||||
const objID = objData.replace(/{{ (input|select|multiselect)\(([a-zA-Z0-9]+)\) }}/, '$2');
|
||||
const objID = objData.replace(/{{ (input|select|multiselect)\((\d+(-\d+)?)\) }}/, '$2');
|
||||
if (objType === 'input') {
|
||||
$(e).html($(e).html().replace(objData, `<div class="objective_${objID} medium-3" style="display: inline-block;">
|
||||
$(e).html($(e).html().replace(objData, `
|
||||
<div class="objective_${objID} medium-3" style="display: inline-block;">
|
||||
<input type="text" placeholder="Question${objNum} ${objID}" name="${objID}" class="textbox objective-input">
|
||||
</div>`));
|
||||
</div>
|
||||
`));
|
||||
} else if (objType === 'select') {
|
||||
$(e).html($(e).html().replace(objData, `Question${objNum}_${objID}:`));
|
||||
$(e).next('ul').children().each((j, ele) => {
|
||||
$(ele).after(`<div class="objective_${objID} radiobox">
|
||||
${String.fromCharCode(65 + j)}.
|
||||
$(ele).after(`
|
||||
<div class="objective_${objID} radiobox">
|
||||
<input type="radio" name="${objID}" class="objective-input" value="${String.fromCharCode(65 + j)}">
|
||||
${ele.innerHTML}
|
||||
${String.fromCharCode(65 + j)}. ${ele.innerHTML}
|
||||
</div>`);
|
||||
$(ele).remove();
|
||||
});
|
||||
} else if (objType === 'multiselect') {
|
||||
$(e).html($(e).html().replace(objData, `Question${objNum}_${objID}:`));
|
||||
$(e).next('ul').children().each((j, ele) => {
|
||||
$(ele).after(`<div class="objective_${objID} radiobox">
|
||||
${String.fromCharCode(65 + j)}.
|
||||
$(ele).after(`
|
||||
<div class="objective_${objID} radiobox">
|
||||
<input type="checkbox" name="${objID}" class="objective-input" value="${String.fromCharCode(65 + j)}">
|
||||
${ele.innerHTML}
|
||||
</div>`);
|
||||
${String.fromCharCode(65 + j)}. ${ele.innerHTML}
|
||||
</div>
|
||||
`);
|
||||
$(ele).remove();
|
||||
});
|
||||
}
|
||||
@@ -345,7 +319,6 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
|
||||
$('[name="problem-sidebar__download"]').on('click', handleClickDownloadProblem);
|
||||
const { type } = UiContext.pdoc.config || {};
|
||||
if (type === 'objective') loadObjective();
|
||||
if (type === 'objectiveNew') loadObjectiveNew();
|
||||
if (pagename !== 'contest_detail_problem') initChart();
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ export default async function readYamlCases(cfg: Record<string, any> = {}, check
|
||||
judge_extra_files: [],
|
||||
user_extra_files: [],
|
||||
};
|
||||
if (cfg.type === 'objective') {
|
||||
if (cfg.checker || cfg.interactor) throw new Error('You cannot use checker or interactor for objective questions');
|
||||
} else {
|
||||
if (cfg.checker) {
|
||||
if (!cfg.checker.includes('.')) {
|
||||
config.checker = findFileSync(`@hydrooj/hydrojudge/vendor/testlib/checkers/${cfg.checker}.cpp`, false);
|
||||
@@ -21,7 +24,7 @@ export default async function readYamlCases(cfg: Record<string, any> = {}, check
|
||||
config[`${n}_extra_files`] = conf.map((file) => checkFile(file, `Cannot find ${n} extra file {0}.`));
|
||||
} else throw new Error(`Invalid ${n}_extra_files config.`);
|
||||
});
|
||||
if (cfg.outputs) config.type = cfg.type || 'objective';
|
||||
}
|
||||
if (cfg.cases?.length) {
|
||||
config.subtasks = [{
|
||||
cases: cfg.cases,
|
||||
@@ -30,7 +33,6 @@ export default async function readYamlCases(cfg: Record<string, any> = {}, check
|
||||
}
|
||||
if (cfg.time) config.time = parseTimeMS(cfg.time);
|
||||
if (cfg.memory) config.memory = parseMemoryMB(cfg.memory);
|
||||
if (config.type === 'objective' && !cfg.outputs?.length) throw new Error('outputs config not found');
|
||||
return Object.assign(cfg, config);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user