core: add IOI(Strict) contest mode (#281) (#525)

This commit is contained in:
panda authored and GitHub committed 2023-03-03 17:52:48 +08:00
1 parent d7ce25827e
commit 476b17153a
5 files changed
+74 -9

No files matched your search

+1 -1
View File
@@ -79,7 +79,7 @@ export async function postJudge(rdoc: RecordDoc) {
if (rdoc.contest) {
await contest.updateStatus(
rdoc.domainId, rdoc.contest, rdoc.uid, rdoc._id,
rdoc.pid, rdoc.status, rdoc.score,
rdoc.pid, rdoc.status, rdoc.score, rdoc.subtasks,
);
} else if (accept && updated) await domain.incUserInDomain(rdoc.domainId, rdoc.uid, 'nAccept', 1);
const isNormalSubmission = ![
+69 -7
View File
@@ -7,13 +7,13 @@ import {
} from '../error';
import {
BaseUserDict, ContestRule, ContestRules, ProblemDict,
ScoreboardNode, ScoreboardRow, Tdoc,
ScoreboardNode, ScoreboardRow, SubtaskResult, Tdoc,
} from '../interface';
import ranked from '../lib/rank';
import * as bus from '../service/bus';
import type { Handler } from '../service/server';
import { buildProjection } from '../utils';
import { PERM, STATUS } from './builtin';
import { PERM, STATUS, STATUS_SHORT_TEXTS } from './builtin';
import * as document from './document';
import problem from './problem';
import RecordModel from './record';
@@ -356,6 +356,64 @@ const ioi = buildContestRule({
showScoreboard: (tdoc, now) => now > tdoc.beginAt,
}, oi);
const strictioi = buildContestRule({
TEXT: 'IOI(Strict)',
submitAfterAccept: false,
showRecord: (tdoc, now) => now > tdoc.endAt,
showSelfRecord: () => true,
showScoreboard: (tdoc, now) => now > tdoc.endAt,
stat(tdoc, journal) {
const detail = {};
let score = 0;
const subtasks: Record<number, SubtaskResult> = {};
for (const j of journal.filter((i) => tdoc.pids.includes(i.pid))) {
Object.keys(j.subtasks).forEach((i) => {
if (!subtasks[i] || subtasks[i].score < j.subtasks[i].score) {
subtasks[i] = j.subtasks[i];
subtasks[i].rid = j.rid;
}
});
j.score = sumBy(Object.values(subtasks), 'score');
j.status = Math.max(...Object.values(subtasks).map((i) => i.status));
j.subtasks = subtasks;
if (!detail[j.pid] || detail[j.pid].score < j.score) detail[j.pid] = j;
}
for (const i in detail) score += detail[i].score;
return { score, detail };
},
async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc, meta) {
const tsddict = tsdoc.detail || {};
const row: ScoreboardNode[] = [
{ type: 'rank', value: rank.toString() },
{ type: 'user', value: udoc.uname, raw: tsdoc.uid },
];
if (isExport) {
row.push({ type: 'email', value: udoc.mail });
row.push({ type: 'string', value: udoc.school || '' });
row.push({ type: 'string', value: udoc.displayName || '' });
row.push({ type: 'string', value: udoc.studentId || '' });
}
row.push({ type: 'total_score', value: tsdoc.score || 0 });
for (const s of tsdoc.journal || []) {
if (!pdict[s.pid]) continue;
pdict[s.pid].nSubmit++;
if (s.status === STATUS.STATUS_ACCEPTED) pdict[s.pid].nAccept++;
}
for (const pid of tdoc.pids) {
row.push({
type: 'record',
value: tsddict[pid]?.penaltyScore || '',
hover: Object.values(tsddict[pid]?.subtasks).map((i: SubtaskResult) => `${STATUS_SHORT_TEXTS[i.status]} ${i.score}`).join(','),
raw: tsddict[pid]?.rid,
style: tsddict[pid]?.status === STATUS.STATUS_ACCEPTED && tsddict[pid]?.rid.getTimestamp().getTime() === meta?.first?.[pid]
? 'background-color: rgb(217, 240, 199);'
: undefined,
});
}
return row;
},
}, ioi);
const ledo = buildContestRule({
TEXT: 'Ledo',
check: () => { },
@@ -576,7 +634,7 @@ const homework = buildContestRule({
});
export const RULES: ContestRules = {
acm, oi, homework, ioi, ledo,
acm, oi, homework, ioi, ledo, strictioi,
};
function _getStatusJournal(tsdoc) {
@@ -633,14 +691,18 @@ export async function getStatus(domainId: string, tid: ObjectId, uid: number) {
return await document.getStatus(domainId, document.TYPE_CONTEST, tid, uid);
}
async function _updateStatus(tdoc: Tdoc<30>, uid: number, rid: ObjectId, pid: number, status: STATUS, score: number) {
async function _updateStatus(
tdoc: Tdoc<30>, uid: number, rid: ObjectId, pid: number, status: STATUS, score: number,
subtasks: Record<number, SubtaskResult>,
) {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
if (isLocked(tdoc)) {
status = STATUS.STATUS_WAITING;
score = 0;
subtasks = {};
}
const tsdoc = await document.revPushStatus(tdoc.domainId, document.TYPE_CONTEST, tdoc.docId, uid, 'journal', {
rid, pid, status, score,
rid, pid, status, score, subtasks,
}, 'rid');
const journal = _getStatusJournal(tsdoc);
const stats = RULES[tdoc.rule].stat(tdoc, journal);
@@ -649,10 +711,10 @@ async function _updateStatus(tdoc: Tdoc<30>, uid: number, rid: ObjectId, pid: nu
export async function updateStatus(
domainId: string, tid: ObjectId, uid: number, rid: ObjectId, pid: number,
status = STATUS.STATUS_WRONG_ANSWER, score = 0,
status = STATUS.STATUS_WRONG_ANSWER, score = 0, subtasks: Record<number, SubtaskResult> = {},
) {
const tdoc = await get(domainId, tid);
return await _updateStatus(tdoc, uid, rid, pid, status, score);
return await _updateStatus(tdoc, uid, rid, pid, status, score, subtasks);
}
export async function getListStatus(domainId: string, uid: number, tids: ObjectId[]) {
+1
View File
@@ -447,6 +447,7 @@ Invalid password for user {0}.: 用户 {0} 的密码错误。
Invalid: 无效
Invitation Code: 邀请码
IO: 输入输出
IOI(Strict): IOI(严格)
Is rated: 计入排名
It works!: 它成了!
Italic: 斜体
@@ -158,7 +158,7 @@
value:tdoc.allowViewCode|default(true),
row:false
}) }}
<div style="display: none" class="contest-rule-settings contest-rule--acm">
<div style="display: none" class="contest-rule-settings contest-rule--acm contest-rule--ioi contest-rule--strictioi">
{{ form.form_text({
columns:4,
label:'Lock (minutes)',
@@ -115,6 +115,8 @@
<p>{{ _('Ledo') }} 赛制下,多次提交会导致选手的得分被扣除,排行榜将显示用户的最高得分。</p>
<p>{{ _('Ledo') }} 赛制下,每道题的最终得分为: $s \times \max(0.95^{n}, 0.7)$ 。$s,n$ 分别代表本次得分和本次提交前的尝试次数。</p>
<p>{{ _('Ledo') }} 排名规则为:按照如上规则折算后的分数从高到低排名。</p>
<p>{{ _('IOI(Strict)') }} 赛制下,不同于IOI赛制,排行榜将被关闭至比赛结束。</p>
<p>{{ _('IOI(Strict)') }} 赛制下,每道题的排行榜得分将为用户每个子任务在所有提交中的最大得分的和。</p>
<p>时间与空间限制以题目说明为准,默认限制参见<a href="#limits">{{ _('Limitations') }}</a>。</p>
</div>
</div>