目录
前言
最近闲得无聊,想用c++自己写一个评测机。一来为了提升个人c++实际应用能力,二来也能学习到不少知识。(其实纯属是为了玩儿)。
本评测机用纯c++编写,平台:devc++。可评测c++/c的程序,编译基于Mingw。由于用纯c++编写,什么乱七八糟的库都没用,所以本评测机会很(方便使用)粗糙,但对于简单的评测来说绰绰有余。模拟场景也没有那么细致,不过,开源嘛,有兴趣的读者可以自行改进,毕竟我写的没有那么好。
实现思路
大体思路就是对拍,不过有很多可以细化的东西。
1.找到指定地址中的目标文件(std.cpp,ans.cpp,存时、空限制和分值的txt,测试数据等等),读取测试数据。
2.分别编译ans.cpp,std.cpp(一个system函数就好了)ps:后面的版本可能会有两种评测方式,一种是两个cpp对拍,一种是cpp和给定的txt答案对拍。
3.将测试数据分别输入两个exe中(只要在cpp文件里加入文件读写就好了),得到输出存入txt中,将两个输出比较。
4.重复3,就可以实现多组测试数据了。
5.后期版本会支持多题测试以及多名选手测试,实用性也会提高。
具体实现
下面我们来具体实现。
对于最初始的版本,我们先把ans.cpp、std.cpp、textpoint.txt和测试样例和评测机放在同一个文件夹下,后续再进行优化。这样我们就可以直接对文件进行操作,不需要获得文件的地址了。
首先我们先编译ans.cpp和std.cpp,让他们生成相应的exe文件。我们需要用到一个函数:
system("g++ -o 目标文件.exe 目标文件.cpp")
这个函数可以对目标cpp文件进行编译,并生成.exe文件。
但是文件也有可能编译错误,这时候就要进行特判。定义一个status_ans变量记录ans.cpp的编译结果。同样,status_std来记录std.cpp的编译结果。
if(status_ans == -1 || status_std == -1 || WIFEXITED(status_ans) == false || WIFEXITED(status_std) == false || 0 != WEXITSTATUS(status_ans) || 0 != WEXITSTATUS(status_std))
用这个语句判断编译错误。若编译错误,则输出”CE“。
编译时还要对电脑是64为还是32为进行判断。具体判断方法:
if(sizeof(char*) == 4)
那么就是32位,否则为64位。如果是32位,那么就要将编译函数改为
system("g++ -o std.exe -m32 std.cpp")
以上是编译部分。编译成功后就要进行评测,评测之前还要读取测试点信息,如测试点时间限制,分值,以及测试点个数。这些都将提前存放在textpoint.txt中。
为了方便读取,存放是我们按一定规则:第一行是测试点个数,接每个测试数据第一行为# + n + 冒号,就代表以下是第n个测试点的数据。每个测试点的数据包含两个整数1:timelimit(单位ms),2:score(自定义数值),中间用一个逗号分开。如下:
3
#1:
100,30
#2:
600,30
#3:
600,40
这就代表有3个数据,第一个时间限制为100毫秒,分值为30分,以此类推。
这是获得测试数据的函数:
void getpoint(const char *filename){
int i, j = 0;
FILE *p;
p = fopen(filename, "r");
fscanf(p, "%[^$]", textpoint);
fclose(p);
for(int i = 0; i < strlen(textpoint); i++){
textpoint[j++] = textpoint[i];
}
textpoint[j] = 0;
int f;
for(int i = 0; i < strlen(textpoint); i++){//获得测试点个数
if(textpoint[i] == '#'){
f = i;
break;
}
if(textpoint[i] >= '0' && textpoint[i] <= '9')
cnt = cnt * 10 + textpoint[i] - '0';
}
int now = 0;
for(int i = f; i <= strlen(textpoint); i++){//获取测试点时间限制及分数
if(textpoint[i] == '#'){
now++;
}
else if(textpoint[i] == ':'){
int nowlimit = 0;
for(int j = i; j <= strlen(textpoint); j++){
if(textpoint[j] == ','){
int nowpoint = 0;
for(int k = j; k <= strlen(textpoint); k++){
if(textpoint[k] == '#'){
break;
}
if(textpoint[k] <= '9' && textpoint[k] >= '0'){
nowpoint = nowpoint * 10 + textpoint[k] - '0';
}
}
sample[now].point = nowpoint;
break;
}
if(textpoint[j] >= '0' && textpoint[j] <= '9') nowlimit = nowlimit * 10 + textpoint[j] - '0';
}
sample[now].time_limit = nowlimit;
}
}
}
获得测试数据后,我们就对每一个测试点进行评测。首先要获得输入数据,我们规定每个输入数据名称格式为:sample + 数字 + .in,例如sample1.in即为第一个测试点的输入数据。
有了测试数据文件名称后,我们把输入数据存入test.in中,方便.cpp文件读取。存入数据这个操作需要掌握文件操作与字符串,用fscanf()函数将sample中的内容读到字符串s中,如果想要忽略空格就把s中的“\n”删除。具体看代码:
void printsample(const char *filename, const char *filename2){
int i, j = 0;
char s[10000];
FILE *p;
p = fopen(filename, "r");
fscanf(p, "%[^$]", s);
fclose(p);
for(int i = 0; i < strlen(s); i++){
s[j++] = s[i];
}
s[j] = 0;
p = fopen(filename2, "w");
fprintf(p, "%s", s);
fclose(p);
return;
}
然后只需要运行.exe文件
system("ans.exe")
就能在ans.out和std.out中看见输出了(真是万能的system)。我们还要判断RE,与判断CE类似。
outcheck_std = system("std.exe");
if(outcheck_std == -1 || outcheck_ans == -1 || WIFEXITED(outcheck_std) == false || WIFEXITED(outcheck_ans) == false || 0 != WEXITSTATUS(outcheck_std) || 0 != WEXITSTATUS(outcheck_ans)){
color(5);
cout << "RE";
color(7);
cout << "(" << elapsed_secs << "ms)\n";
continue;
}
最后我们将两个输出进行对比,如果相同,记为AC,并且加上对应测试点的分数,否则WA,还有忽略空格、换行什么的,再进行处理就行了。这是处理输出的函数
void enter_deal(const char* filename, int Type = 0){
int i, j = 0;
char s[10000];
FILE *p;
p = fopen(filename, "r");
fscanf(p, "%[^$]", s);
fclose(p);
for(int i = 0; i < strlen(s); i++){
if(Type == 0 && s[i] == ' ' || s[i] == '\n')
continue;
s[j++] = s[i];
}
s[j] = 0;
if(Type == 0){
p = fopen(filename, "w");
fprintf(p, "%s", s);
fclose(p);
}
return;
}
然后将处理好的字符串对比就行了。
我们还有一个数据没有用到:时间限制。怎么实现呢?我们只要在运行ans.exe之前记录一次时间,运行后再记录一次,两次详见,就能获得运行时间了(单位:ms)。具体如下:
start = clock();
outcheck_ans = system("ans.exe");
end = clock();//用end-start(注意是double),就能获得时间了。
获得了运行时间后再与测试数据中的时间限制进行对比,就能判断是否TLE了。
另外,ans.cpp和std.cpp的main函数中要加入这么两行
freopen("test.in","r",stdin);
freopen("ans.out","w",stdout);
参加过csp的同学应该知道,这是文件读取。方便从test.in中读取输入内容,并将答案输出到ans.out/std.out中,方便评测机进行评测。
评测机完善
最后还有一点小细节,算是对评测机的完善。因为评测及到现在还是很粗糙的,所以我们来精细一下。
比如我们可以删除用户快速编辑(就是鼠标选中功能),这样来更好的模拟软件的界面,而不是控制台界面,但这知识相对于windows用户而言的。以下是相关函数:
void Clear_Qiuckedit(){
HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
DWORD mode;
GetConsoleMode(hStdin, &mode);
mode &= ~ENABLE_QUICK_EDIT_MODE;
SetConsoleMode(hStdin, mode);
}
还有就是隐藏鼠标,让界面看起来更加图形化
void hide(int hide_type = 0){
CONSOLE_CURSOR_INFO cursor_info = {1, hide_type};
SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cursor_info);
}
我们还可以通过改变字体颜色,对于每个测试结果(AC,WA,TLE...)都输出不同的颜色,这样看起来更加美观,也更有评测机那味儿,而不是像bat。
void color(int x, bool intensity = true){
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),
x | (intensity << 3));
}
再附上最后的效果图:
怎么样,是不是有那味了。
使用方法
评测机到现在差不多完善了,后续也会有改进版本。下面来介绍一下使用方法。
首先,新建一个文件夹,把评测机放进去,在把待评测的cpp文件放进去,并命名为ans.cpp,再把标程放进去,命名为std.cpp。在ans.cpp的主函数第一行中加入以下代码:
freopen("test.in","r",stdin);
freopen("ans.out","w",stdout);
在std.cpp的主函数第一行加入以下代码:
freopen("test.in","r",stdin);
freopen("std.out","w",stdout);
然后,新建一个txt文件,命名为textpoint.txt。在里面按照格式写入测试数据,具体格式上文有提到过,这里就不再赘述了。
最后,根据测试点的个数,新建对应个数的.in文件,分别命名为sample1.in,sample2.in等等,代表第一个输入数据,第二个输入数据...将输入数据写入.in文件中。
评测机环境就配置好了,最后只要运行评测机,就可以进行评测了。
完整代码
最后就是喜闻乐见的代码了。
//v1.0.0
#include<bits/stdc++.h>
#include <windows.h>
#define WEXITSTATUS(status) (((status) & 0xff00) >> 8)
#define WIFEXITED(status) ((status & 0x7f) == 0)
using namespace std;
char textpoint[10000];
struct node{
int time_limit, point;
}sample[1005];
int cnt, totle;
int outcheck_std, outcheck_ans;
const int maxn = 100005;
char ANS[maxn], STD[maxn] = "";
clock_t start, end;
double elapsed_secs;
char s = 'k';
const char *name1 = "test.in";
string samplename = "sample";
void Clear_Qiuckedit(){
HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
DWORD mode;
GetConsoleMode(hStdin, &mode);
mode &= ~ENABLE_QUICK_EDIT_MODE;
SetConsoleMode(hStdin, mode);
}
void hide(int hide_type = 0){
CONSOLE_CURSOR_INFO cursor_info = {1, hide_type};
SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cursor_info);
}
void color(int x, bool intensity = true){
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),
x | (intensity << 3));
}
void gotoxy(int x, int y){
COORD pos = {y - 1, x - 1};
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos);
}
void print(string s, int n){
gotoxy(1, 1);
cout << "状态:";
color(n);
cout << s << " " << endl;
color(7);
}
void enter_deal(const char* filename, int Type = 0){
int i, j = 0;
char s[10000];
FILE *p;
p = fopen(filename, "r");
fscanf(p, "%[^$]", s);
fclose(p);
for(int i = 0; i < strlen(s); i++){
if(Type == 0 && s[i] == ' ' || s[i] == '\n')
continue;
s[j++] = s[i];
}
s[j] = 0;
if(Type == 0){
p = fopen(filename, "w");
fprintf(p, "%s", s);
fclose(p);
}
return;
}
void getpoint(const char *filename){
int i, j = 0;
FILE *p;
p = fopen(filename, "r");
fscanf(p, "%[^$]", textpoint);
fclose(p);
for(int i = 0; i < strlen(textpoint); i++){
textpoint[j++] = textpoint[i];
}
textpoint[j] = 0;
int f;
for(int i = 0; i < strlen(textpoint); i++){//获得测试点个数
if(textpoint[i] == '#'){
f = i;
break;
}
if(textpoint[i] >= '0' && textpoint[i] <= '9')
cnt = cnt * 10 + textpoint[i] - '0';
}
int now = 0;
for(int i = f; i <= strlen(textpoint); i++){//获取测试点时间限制及分数
if(textpoint[i] == '#'){
now++;
}
else if(textpoint[i] == ':'){
int nowlimit = 0;
for(int j = i; j <= strlen(textpoint); j++){
if(textpoint[j] == ','){
int nowpoint = 0;
for(int k = j; k <= strlen(textpoint); k++){
if(textpoint[k] == '#'){
break;
}
if(textpoint[k] <= '9' && textpoint[k] >= '0'){
nowpoint = nowpoint * 10 + textpoint[k] - '0';
}
}
sample[now].point = nowpoint;
break;
}
if(textpoint[j] >= '0' && textpoint[j] <= '9') nowlimit = nowlimit * 10 + textpoint[j] - '0';
}
sample[now].time_limit = nowlimit;
}
}
}
void printsample(const char *filename, const char *filename2){
int i, j = 0;
char s[10000];
FILE *p;
p = fopen(filename, "r");
fscanf(p, "%[^$]", s);
fclose(p);
for(int i = 0; i < strlen(s); i++){
s[j++] = s[i];
}
s[j] = 0;
p = fopen(filename2, "w");
fprintf(p, "%s", s);
fclose(p);
return;
}
int main(){
Clear_Qiuckedit();
hide();
color(7);
FILE *fp;
if((fp = fopen("textpoint.txt", "r")) == NULL){
printf("请创建textpoin.txt并将测试点数据存入\n");
return 0;
}
fclose(fp);
getpoint("textpoint.txt");
print("提交中", 7);
int i = 0, j = 0;
gotoxy(1, 1);
print("编译中", 7);
if(sizeof(char*) == 4){
int status_std32 = 0, status_ans32 = 0;
status_std32 = system("g++ -o std.exe -m32 std.cpp");
status_ans32 = system("g++ -o ans.exe -m32 ans.cpp");
if(status_ans32 == -1 || status_std32 == -1 || WIFEXITED(status_ans32) == false || WIFEXITED(status_std32) == false || 0 != WEXITSTATUS(status_ans32) || 0 != WEXITSTATUS(status_std32)){
print("CE", 6);
return 0;
}
}
if(sizeof(char*) == 8){
int status_std = 0, status_ans = 0;
status_std = system("g++ -o std.exe std.cpp");
status_ans = system("g++ -o ans.exe ans.cpp");
if(status_ans == -1 || status_std == -1 || WIFEXITED(status_ans) == false || WIFEXITED(status_std) == false || 0 != WEXITSTATUS(status_ans) || 0 != WEXITSTATUS(status_std)){
print("CE", 6);
return 0;
}
}
print("评测中", 7);
for(int i = 1; i <= cnt; i++){
cout << "#" << i << ": ";
char temp[10000];
sprintf(temp, "%d", i);
string toname = samplename + temp + ".in";
const char *name2 = toname.c_str();
printsample(name2, name1);
outcheck_std = system("std.exe");
start = clock();
outcheck_ans = system("ans.exe");
end = clock();
elapsed_secs = static_cast<double>(end - start);
if(outcheck_std == -1 || outcheck_ans == -1 || WIFEXITED(outcheck_std) == false || WIFEXITED(outcheck_ans) == false || 0 != WEXITSTATUS(outcheck_std) || 0 != WEXITSTATUS(outcheck_ans)){
color(5);
cout << "RE";
color(7);
cout << "(" << elapsed_secs << "ms)\n";
continue;
}
if(elapsed_secs > sample[i].time_limit){
color(3);
cout << "TLE";
color(7);
cout << "(" << elapsed_secs << "ms)\n";
continue;
}
enter_deal("ans.out");
enter_deal("std.out");
int count_ans = 0, count_std = 0;
freopen("std.out", "r", stdin);
scanf("%s", STD);
freopen("ans.out", "r", stdin);
scanf("%s", ANS);
int len_a = strlen(ANS);
int len_s = strlen(STD);
if(strcmp(ANS, STD) == 0){
color(2);
cout << "AC";
color(7);
cout << "(" << elapsed_secs << "ms)\n";
totle += sample[i].point;
}
else{
color(4);
cout << "WA";
color(7);
cout << "(" << elapsed_secs << "ms)\n";
}
}
cout << "分数:" << totle << endl;
return 0;
}
代码具体实现过程在上文都已经讲了,还有不懂得可以在评论区论里问我。里面还有很多小细节没有讲到,不过看代码应该能看懂,最主要是学习嘛。代码中有不少关于文件操作的函数,相信理解了代码之后,你关于文件操作也能掌握不少。
还有就是,都看到最后了,留下你的三连再走呗~。