From 76745639c10e00b39c9ea566df39a437df0ddf19 Mon Sep 17 00:00:00 2001 From: undefined Date: Mon, 27 Jun 2022 15:00:58 +0800 Subject: [PATCH] judge: change objective config format --- packages/hydrojudge/src/cases.ts | 2 +- packages/hydrojudge/src/judge/interface.ts | 5 +- packages/hydrojudge/src/judge/objective.ts | 109 +++++++++++++++--- packages/hydrooj/src/interface.ts | 2 +- .../components/monaco/languages/yaml.ts | 13 ++- .../ui-default/pages/problem_detail.page.jsx | 65 +++-------- packages/utils/lib/cases.ts | 30 ++--- 7 files changed, 139 insertions(+), 87 deletions(-) diff --git a/packages/hydrojudge/src/cases.ts b/packages/hydrojudge/src/cases.ts index baf38cd5d..d28917de0 100644 --- a/packages/hydrojudge/src/cases.ts +++ b/packages/hydrojudge/src/cases.ts @@ -88,7 +88,7 @@ export default async function readCases(folder: string, cfg: Record 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); diff --git a/packages/hydrojudge/src/judge/interface.ts b/packages/hydrojudge/src/judge/interface.ts index 1ebb1f78b..3f34a6688 100644 --- a/packages/hydrojudge/src/judge/interface.ts +++ b/packages/hydrojudge/src/judge/interface.ts @@ -62,10 +62,7 @@ export interface Config { hack?: string; interactor?: string; - outputs?: { - output: string, - score: number, - }[]; + answers?: Record; } export interface ContextSubTask { diff --git a/packages/hydrojudge/src/judge/objective.ts b/packages/hydrojudge/src/judge/objective.ts index 0d6a374fb..2acaa0d64 100644 --- a/packages/hydrojudge/src/judge/objective.ts +++ b/packages/hydrojudge/src/judge/objective.ts @@ -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(totalStatus, STATUS.STATUS_ACCEPTED); + next({ + status: totalStatus, + case: { + ...baseInfo, + status: STATUS.STATUS_ACCEPTED, + score, + message: 'Correct', + }, + }, +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); + } } - totalScore += score; - totalStatus = Math.max(status, totalStatus); - next({ - status: totalStatus, - progress: (100 * (+i + 1)) / config.outputs.length, - case: { - status, score, time: 0, memory: 0, message: '', - }, - }, +i + 1); } return end({ status: totalStatus, score: totalScore, time: 0, memory: 0, diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index c8e29de2b..df67241c5 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -170,7 +170,7 @@ export interface ProblemConfigFile { user_extra_files?: string[]; judge_extra_files?: string[]; detail?: boolean; - outputs?: [string, number][]; + answers?: Record; redirect?: string; cases?: TestCaseConfig[]; subtasks?: SubtaskConfig[]; diff --git a/packages/ui-default/components/monaco/languages/yaml.ts b/packages/ui-default/components/monaco/languages/yaml.ts index 84a576182..df6e9a887 100644 --- a/packages/ui-default/components/monaco/languages/yaml.ts +++ b/packages/ui-default/components/monaco/languages/yaml.ts @@ -70,9 +70,16 @@ const problemConfigSchema = { }, additionalProperties: false, }, - outputs: { - type: 'array', - minLength: 1, + answers: { + type: 'object', + patternProperties: { + '^\\d+(-\\d+)?$': { + type: 'array', + minLength: 2, + maxLength: 2, + }, + }, + additionalProperties: false, }, }, additionalProperties: false, diff --git a/packages/ui-default/pages/problem_detail.page.jsx b/packages/ui-default/pages/problem_detail.page.jsx index b3cca2cf3..c18663d44 100644 --- a/packages/ui-default/pages/problem_detail.page.jsx +++ b/packages/ui-default/pages/problem_detail.page.jsx @@ -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( -
- -
, - ); - $('.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, `
- -
`)); + $(e).html($(e).html().replace(objData, ` +
+ +
+ `)); } else if (objType === 'select') { $(e).html($(e).html().replace(objData, `Question${objNum}_${objID}:`)); $(e).next('ul').children().each((j, ele) => { - $(ele).after(`
- ${String.fromCharCode(65 + j)}. - - ${ele.innerHTML} -
`); + $(ele).after(` +
+ + ${String.fromCharCode(65 + j)}. ${ele.innerHTML} +
`); $(ele).remove(); }); } else if (objType === 'multiselect') { $(e).html($(e).html().replace(objData, `Question${objNum}_${objID}:`)); $(e).next('ul').children().each((j, ele) => { - $(ele).after(`
- ${String.fromCharCode(65 + j)}. - - ${ele.innerHTML} -
`); + $(ele).after(` +
+ + ${String.fromCharCode(65 + j)}. ${ele.innerHTML} +
+ `); $(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(); }); diff --git a/packages/utils/lib/cases.ts b/packages/utils/lib/cases.ts index faa5a0bea..72980e132 100644 --- a/packages/utils/lib/cases.ts +++ b/packages/utils/lib/cases.ts @@ -7,21 +7,24 @@ export default async function readYamlCases(cfg: Record = {}, check judge_extra_files: [], user_extra_files: [], }; - if (cfg.checker) { - if (!cfg.checker.includes('.')) { - config.checker = findFileSync(`@hydrooj/hydrojudge/vendor/testlib/checkers/${cfg.checker}.cpp`, false); + 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); + } + if (!config.checker) config.checker = checkFile(cfg.checker, 'Cannot find checker {0}.'); } - if (!config.checker) config.checker = checkFile(cfg.checker, 'Cannot find checker {0}.'); + if (cfg.interactor) config.interactor = checkFile(cfg.interactor, 'Cannot find interactor {0}.'); + ['judge', 'user'].forEach((n) => { + const conf = cfg[`${n}_extra_files`]; + if (!conf) return; + if (conf instanceof Array) { + 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.interactor) config.interactor = checkFile(cfg.interactor, 'Cannot find interactor {0}.'); - ['judge', 'user'].forEach((n) => { - const conf = cfg[`${n}_extra_files`]; - if (!conf) return; - if (conf instanceof Array) { - 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 = {}, 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); }