UNPKG

oicontest

Version:

OI Contest Management Tool

316 lines (296 loc) 13.4 kB
import { Command } from 'commander'; import * as fs from 'fs-extra'; import * as path from 'path'; import inquirer from 'inquirer'; import { getTemplate, writeFileWithDir, writeYaml } from '../utils/utils'; import { loadConfig, saveConfig, ProblemConfig, ProblemStatus } from '../lib/config'; import chalk from 'chalk'; import { mkdirp } from 'mkdirp'; export const addProblemCommand = new Command('addproblem') .description('Add a new problem to the contest') .action(async () => { const contestDir = process.cwd(); try { const config = await loadConfig(contestDir); console.log(chalk.bold.blue('\n➕ Add New Problem\n')); const answers = await inquirer.prompt([ { type: 'input', name: 'id', message: 'Problem ID:', validate: input => input.trim() !== '' || 'ID cannot be empty' }, { type: 'input', name: 'title', message: 'Problem title:', default: 'Untitled Problem' }, { type: 'input', name: 'timeLimit', message: 'Time limit (ms):', default: '1000', validate: input => !isNaN(Number(input)) || 'Please enter a valid number' }, { type: 'input', name: 'memoryLimit', message: 'Memory limit (MB):', default: '256', validate: input => !isNaN(Number(input)) || 'Please enter a valid number' }, { type: "checkbox", name: "tags", message: "请选择题目标签:", choices: [ { name: "入门", value: "入门" }, { name: "普及-", value: "普及-" }, { name: "普及/提高-", value: "普及/提高-" }, { name: "普及+/提高", value: "普及+/提高" }, { name: "提高+/省选-", value: "提高+/省选-" }, { name: "省选/NOI-", value: "省选/NOI-" }, { name: "NOI/NOI+/CTSC", value: "NOI/NOI+/CTSC" }, new inquirer.Separator(), { name: "动态规划", value: "动态规划" }, { name: "图论", value: "图论" }, { name: "数据结构", value: "数据结构" }, { name: "数学", value: "数学" }, { name: "基础算法", value: "基础算法" }, { name: "字符串", value: "字符串" }, { name: "其他", value: "其他" }, ], default: ["入门"], }, { type: "input", name: "othertag", message: "其他分类标签(用空格或,分割多个):", default: "", }, { type: 'input', name: 'maxScore', message: 'Maximum score:', default: '100', validate: input => !isNaN(Number(input)) || 'Please enter a valid number' } ]); // Check if problem exists if (config.problems.some(p => p.id === answers.id)) { console.error(chalk.red(`\nError: Problem "${answers.id}" already exists`)); process.exit(1); } const problemDir = path.join(contestDir,'problem', answers.id); // Create problem directories // fs.ensureDirSync(path.join(problemDir, 'testdata')); // 测试数据 // fs.ensureDirSync(path.join(problemDir, 'src')); // 源文件,包括std等 // fs.ensureDirSync(path.join(problemDir, 'sample')); // 样例文件,包括大样例 // fs.ensureDirSync(path.join(problemDir, 'solution')); // 题解 // // Create default files // fs.writeFileSync( // path.join(problemDir, 'problem.md'), // `# ${answers.title}\n\n## Problem Description\n\n` // ); // 选择checker类型 // const checkerType = await inquirer.prompt([ { type: "list", name: "selectedChecker", message: "请选择需要的Checker 类型:", choices: [ { name: "ncmp (标准比较器:逐字节对比用户输出和标准答案,但会忽略行末空格和文件末尾的多余换行)", value: "ncmp.cpp", }, { name: "wcmp (比较两个单词序列,按顺序逐个比较单词,若某对单词不同或序列长度不同,则判定为答案错误;否则判定为答案正确。)", value: "wcmp.cpp", }, { name: "fcmp (将文件按行作为字符串序列进行比较,若某行内容不同,则判定为答案错误;否则判定为答案正确。)", value: "fcmp.cpp", }, { name: "rcmp (比较两个双精度浮点数,允许最大绝对误差为 1.5E-6。若误差超过该值,判定为答案错误;否则判定为答案正确。)", value: "rcmp.cpp", }, { name: "rcmp4 (比较两个双精度浮点数序列,允许最大绝对或相对误差为 1E-4。若某对元素误差超过该值,判定为答案错误;否则判定为答案正确。)", value: "rcmp4.cpp", }, { name: "rcmp6 (比较两个双精度浮点数序列,允许最大绝对或相对误差为 1E-6。若某对元素误差超过该值,判定为答案错误;否则判定为答案正确。)", value: "rcmp6.cpp", }, { name: "rcmp9 (比较两个双精度浮点数序列,允许最大绝对或相对误差为 1E-9。若某对元素误差超过该值,判定为答案错误;否则判定为答案正确。)", value: "rcmp9.cpp", }, { name: "rncmp (比较两个双精度浮点数序列,允许最大绝对误差为 1.5E-5。若某对元素误差超过该值,判定为答案错误;否则判定为答案正确。)", value: "rncmp.cpp", }, { name: "uncmp (比较两个无序的有符号长整型序列,会先排序再比较,若序列长度或元素不同,则判定为错误。)", value: "uncmp.cpp", }, { name: 'yesno (检查输入是否为 "YES" 或 "NO" (大小写不敏感),若输入不符合要求或与答案不一致,则判定为错误。)', value: "yesno.cpp", }, { name: "acmp (比较两个双精度浮点数,允许最大绝对误差为 1.5E-6。若误差超过该值,判定为答案错误;否则判定为答案正确。)", value: "acmp.cpp", }, { name: "caseicmp (带有测试用例支持的单 int64 检查器,用于比较两个有序的 int64 序列,若序列长度不同或对应元素不同,则判定为错误。)", value: "caseicmp.cpp", }, { name: 'casencmp (用于比较输出和答案的格式为 "Case X: <number> <number> ..." 的情况,按测试用例逐行比较长整型序列。)', value: "casencmp.cpp", }, { name: 'casewcmp (用于比较输出和答案的格式为 "Case X: <token> <token> ..." 的情况,按测试用例逐行比较字符串序列。)', value: "casewcmp.cpp", }, { name: "dcmp (比较两个双精度浮点数,允许最大绝对或相对误差为 1E-6。若误差超过该值,判定为答案错误;否则判定为答案正确。)", value: "dcmp.cpp", }, { name: "hcmp (比较两个有符号的大整数,会先检查输入是否为有效的整数格式,若格式错误或数值不同,则判定为错误。)", value: "hcmp.cpp", }, { name: "icmp (比较两个有符号的整数,若两个整数不相等,则判定为答案错误;否则判定为答案正确。)", value: "icmp.cpp", }, { name: "lcmp (将文件按行拆分为单词序列进行比较,若某行的单词序列不同,则判定为答案错误;否则判定为答案正确。)", value: "lcmp.cpp", }, { name: "ncmp (比较两个有序的有符号长整型序列,会检查序列长度和对应元素是否相同,若不同则判定为错误。)", value: "ncmp.cpp", }, { name: 'nyesno (用于检查多个 "YES" 或 "NO" (大小写不敏感)的输入,会统计 "YES" 和 "NO" 的数量,若输入不符合要求或与答案不一致,则判定为错误。)', value: "nyesno.cpp", }, { name: "pointscmp (示例得分检查器,通过比较两个双精度浮点数的差值来给出得分。)", value: "pointscmp.cpp", }, { name: "pointsinfo (示例带有 points_info 的检查器,读取两个双精度浮点数,记录相关信息并退出。)", value: "pointsinfo.cpp", }, ], default: 0, }, ]); // 创建目录结构 const dirs = ["testdata", "src", "sample", "solution", "additional_file"]; for (const dir of dirs) { await mkdirp(path.join(problemDir, dir)); } // 生成题目状态文件 const problemStatus: ProblemStatus = { dir: { desc: "目录完整", status: false }, isvalidated: { desc: "验证输入数据", status: false }, isgenerated: { desc: "评测数据", status: false }, ischecked: { desc: "是否检查完整", status: false } }; try { const statusFileName = path.resolve(problemDir, "status.json"); console.log(statusFileName); fs.writeFileSync(statusFileName, JSON.stringify(problemStatus, null, 2), "utf-8"); } catch (e: unknown) { console.error(chalk.red("写入题目状态文件失败:"), (e as Error).message); process.exit(1); } // 写入模板文件 const templates = { "src/std.cpp": "std.cpp", "problem_zh.md": "problem_zh.md", "src/generator.cpp": "generator.cpp", "src/validator.cpp": "validator.cpp", "src/testlib.h": "testlib.h", "solution/stdsol.md": "solution/stdsol.md", "src/checker.cpp": "checker.cpp", }; // 根据用户选择的checker if (checkerType.value) templates["src/checker.cpp"] = `checkers/${checkerType.value}`; else templates["src/checker.cpp"] = "checkers/wcmp.cpp"; // 根据用户选择过滤模板文件 const finalTemplates = Object.entries(templates).filter(([filePath]) => { if (filePath.includes("checker")&& false) return false; if (filePath.includes("generator") && false) return false; if (filePath.includes("validator") && false) return false; return true; }); const templateDir = path.resolve(__dirname,"../templates"); for (const [filePath, templateName] of finalTemplates) { writeFileWithDir( path.join(problemDir, filePath), getTemplate(templateDir, templateName) ); } // 生成 problem.yaml answers.tags.filter((item: string) => item !== ""); if (answers.othertag) answers["othertag"].split(/[\s,,;]+/).forEach((el: string) => { answers.tags.push(el); }); const problemYaml = { tag: [...answers.tags], title: answers.title, }; writeYaml(path.join(problemDir, "problem.yaml"), problemYaml); // 生成 testdata/config.yaml const configYaml = { memory: answers.memoryLimit, time: answers.timeLimit, }; writeYaml(path.join(problemDir, "testdata", "config.yaml"), configYaml); const problemName = answers.title; console.log(chalk.green(`\n题目 "${problemName}" 初始化完成!`)); console.log(chalk.blue(`目录结构已创建在: ${problemDir}`)); //查找目前有几道题目 const indexs = config.problems.length; const problemConfig: ProblemConfig = { id: answers.id, index:indexs, title: answers.title, timeLimit: parseInt(answers.timeLimit), memoryLimit: parseInt(answers.memoryLimit), maxScore: parseInt(answers.maxScore), }; // Save problem config fs.writeJsonSync( path.join(problemDir, 'config.json'), problemConfig, { spaces: 2 } ); // Update contest config config.problems.push(problemConfig); config.status.problemsAdded = true; await saveConfig(process.cwd(), config); console.log(chalk.green.bold('\n✅ Problem added successfully!')); console.log(chalk.cyan(` ID: ${answers.id}`)); console.log(chalk.cyan(` Title: ${answers.title}`)); console.log(chalk.cyan(` Time Limit: ${answers.timeLimit}ms`)); console.log(chalk.cyan(` Memory Limit: ${answers.memoryLimit}MB`)); console.log(chalk.cyan(` Directory: ${problemDir}`)); } catch (err: unknown) { console.error(chalk.red(`Error adding problem: ${(err as Error).message}`)); process.exit(1); } });