mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 17:36:08 +08:00
core: update rp method
This commit is contained in:
1 parent
ba5dd281dd
commit
9e86d47d19
10 files changed
+136
-122
No files matched your search
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydrooj",
|
||||
"version": "3.7.2",
|
||||
"version": "3.8.0",
|
||||
"bin": "bin/hydrooj.js",
|
||||
"main": "src/loader",
|
||||
"module": "src/loader",
|
||||
|
||||
@@ -619,6 +619,7 @@ export interface Model {
|
||||
user: typeof import('./model/user').default,
|
||||
oauth: typeof import('./model/oauth').default,
|
||||
storage: typeof import('./model/storage').default,
|
||||
rp: typeof import('./script/rating').RpTypes,
|
||||
}
|
||||
|
||||
export interface Service {
|
||||
|
||||
@@ -179,7 +179,7 @@ const acm = buildContestRule({
|
||||
return [rows, udict, nPages];
|
||||
},
|
||||
async ranked(tdoc, cursor) {
|
||||
return await ranked.all(cursor, (a, b) => a.score === b.score);
|
||||
return await ranked.all(cursor, (a, b) => a.accept === b.accept && a.time === b.time);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -161,7 +161,8 @@ DomainUserSetting(
|
||||
Setting('setting_storage', 'nAccept', 0, 'number', 'nAccept', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('setting_storage', 'nSubmit', 0, 'number', 'nSubmit', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('setting_storage', 'nLike', 0, 'number', 'nLike', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('setting_storage', 'rp', 1500, 'number', 'RP', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('setting_storage', 'rp', 0, 'number', 'RP', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('setting_storage', 'rpInfo', '', 'text'/* JSON */, 'RP Detail', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('setting_storage', 'rpdelta', 0, 'number', 'RP.delta', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
Setting('setting_storage', 'rank', 0, 'number', 'Rank', null, FLAG_DISABLED | FLAG_HIDDEN),
|
||||
Setting('setting_storage', 'level', 0, 'number', 'level', null, FLAG_HIDDEN | FLAG_DISABLED),
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/* eslint-disable no-cond-assign */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { NumericDictionary, unionWith } from 'lodash';
|
||||
import { FilterQuery, ObjectID } from 'mongodb';
|
||||
import { ProblemDoc, Tdoc, Udoc } from '../interface';
|
||||
import { FilterQuery } from 'mongodb';
|
||||
import { Tdoc, Udoc } from '../interface';
|
||||
import difficultyAlgorithm from '../lib/difficulty';
|
||||
import rating from '../lib/rating';
|
||||
import { PRIV, STATUS } from '../model/builtin';
|
||||
import * as contest from '../model/contest';
|
||||
import domain from '../model/domain';
|
||||
import problem from '../model/problem';
|
||||
import record from '../model/record';
|
||||
import UserModel from '../model/user';
|
||||
import db from '../service/db';
|
||||
|
||||
@@ -16,77 +16,88 @@ export const description = 'Calculate rp of a domain, or all domains';
|
||||
|
||||
type ND = NumericDictionary<number>;
|
||||
|
||||
async function runProblem(pdoc: ProblemDoc, udict: ND): Promise<void>;
|
||||
async function runProblem(domainId: string, pid: number, udict: ND): Promise<void>;
|
||||
async function runProblem(...arg: any[]) {
|
||||
const pdoc: ProblemDoc = (typeof arg[0] === 'string')
|
||||
? await problem.get(arg[0], arg[1])
|
||||
: arg[0];
|
||||
const udict: ND = (typeof arg[0] === 'string') ? arg[2] : arg[1];
|
||||
const nPages = Math.floor(
|
||||
(await problem.getMultiStatus(
|
||||
pdoc.domainId,
|
||||
{
|
||||
docId: pdoc.docId,
|
||||
rid: { $ne: null },
|
||||
uid: { $ne: pdoc.owner },
|
||||
},
|
||||
).count() + 99) / 100,
|
||||
);
|
||||
pdoc.difficulty = pdoc.difficulty || difficultyAlgorithm(pdoc.nSubmit, pdoc.nAccept) || 5;
|
||||
const p = pdoc.difficulty / (Math.sqrt(Math.sqrt(pdoc.nAccept)) + 1) / 10;
|
||||
for (let page = 1; page <= nPages; page++) {
|
||||
const psdocs = await problem.getMultiStatus(
|
||||
pdoc.domainId, { docId: pdoc.docId, rid: { $ne: null } },
|
||||
).limit(100).skip((page - 1) * 100).project({ rid: 1, uid: 1 }).toArray();
|
||||
const rdict = await record.getList(pdoc.domainId, psdocs.map((psdoc) => psdoc.rid));
|
||||
for (const psdoc of psdocs) {
|
||||
if (rdict[psdoc.rid.toHexString()]) {
|
||||
const rp = rdict[psdoc.rid.toHexString()].score * p;
|
||||
udict[psdoc.uid] = (udict[psdoc.uid] || 1500) + rp;
|
||||
}
|
||||
}
|
||||
}
|
||||
udict[pdoc.owner] = (udict[pdoc.owner] || 1500) + pdoc.difficulty;
|
||||
interface RpDef {
|
||||
run(domainIds: string[], udict: ND, report: Function): Promise<void>;
|
||||
hidden: boolean;
|
||||
base: number;
|
||||
}
|
||||
|
||||
async function runContest(tdoc: Tdoc<30 | 60>, udict: ND, report: Function): Promise<void>;
|
||||
async function runContest(
|
||||
domainId: string, tid: ObjectID, udict: ND, report: Function
|
||||
): Promise<void>;
|
||||
async function runContest(...arg: any[]) {
|
||||
const start = new Date().getTime();
|
||||
const tdoc: Tdoc<30> = (typeof arg[0] === 'string')
|
||||
? await contest.get(arg[0], arg[1])
|
||||
: arg[0];
|
||||
const udict: ND = (typeof arg[0] === 'string') ? arg[2] : arg[1];
|
||||
const report = (typeof arg[0] === 'string') ? arg[3] : arg[2];
|
||||
const cursor = contest.getMultiStatus(tdoc.domainId, {
|
||||
docId: tdoc.docId,
|
||||
journal: { $ne: null },
|
||||
}).sort(contest.RULES[tdoc.rule].statusSort);
|
||||
if (!await cursor.count()) return;
|
||||
const [rankedTsdocs] = await contest.RULES[tdoc.rule].ranked(tdoc, cursor);
|
||||
const users = [];
|
||||
for (const result of rankedTsdocs) {
|
||||
users.push({ uid: result[1].uid, rank: result[0], old: udict[result[1].uid] || 1500 });
|
||||
}
|
||||
// FIXME sum(rating.new) always less than sum(rating.old)
|
||||
const rated = rating(users);
|
||||
for (const udoc of rated) udict[udoc.uid] = udoc.new;
|
||||
await report({
|
||||
case: {
|
||||
status: STATUS.STATUS_ACCEPTED,
|
||||
message: `Contest ${tdoc.title} finished`,
|
||||
time: new Date().getTime() - start,
|
||||
memory: 0,
|
||||
score: 0,
|
||||
export const RpTypes: Record<string, RpDef> = {
|
||||
problem: {
|
||||
async run(domainIds, udict, report) {
|
||||
const problems = await problem.getMulti('', { domainId: { $in: domainIds }, nAccept: { $gt: 0 }, hidden: false }).toArray();
|
||||
if (problems.length) await report({ message: `Found ${problems.length} problems in ${domainIds[0]}` });
|
||||
for (const pdoc of problems) {
|
||||
const cursor = problem.getMultiStatus(
|
||||
pdoc.domainId,
|
||||
{
|
||||
docId: pdoc.docId,
|
||||
rid: { $ne: null },
|
||||
uid: { $ne: pdoc.owner },
|
||||
score: { $gt: 0 },
|
||||
},
|
||||
);
|
||||
const difficulty = +pdoc.difficulty || difficultyAlgorithm(pdoc.nSubmit, pdoc.nAccept) || 5;
|
||||
const p = difficulty / (Math.log(pdoc.nAccept) + 1) / 100;
|
||||
let psdoc;
|
||||
while (psdoc = await cursor.next()) {
|
||||
udict[psdoc.uid] += Math.max(psdoc.score, 100) * p;
|
||||
}
|
||||
}
|
||||
for (const key in udict) udict[key] = Math.max(0, Math.log(udict[key]) / Math.log(1.03));
|
||||
},
|
||||
});
|
||||
}
|
||||
hidden: false,
|
||||
base: 0,
|
||||
},
|
||||
contest: {
|
||||
async run(domainIds, udict, report) {
|
||||
const contests: Tdoc<30>[] = await contest.getMulti('', { domainId: { $in: domainIds }, rated: true })
|
||||
.sort({ _id: 1 }).toArray() as any;
|
||||
if (contests.length) await report({ message: `Found ${contests.length} contests in ${domainIds[0]}` });
|
||||
for (const tdoc of contests) {
|
||||
const start = Date.now();
|
||||
const cursor = contest.getMultiStatus(tdoc.domainId, {
|
||||
docId: tdoc.docId,
|
||||
journal: { $ne: null },
|
||||
}).sort(contest.RULES[tdoc.rule].statusSort);
|
||||
if (!await cursor.count()) return;
|
||||
const [rankedTsdocs] = await contest.RULES[tdoc.rule].ranked(tdoc, cursor);
|
||||
const users = rankedTsdocs.map((i) => ({ uid: i[1].uid, rank: i[0], old: udict[i[1].uid] }));
|
||||
// FIXME sum(rating.new) always less than sum(rating.old)
|
||||
for (const udoc of rating(users)) udict[udoc.uid] = udoc.new;
|
||||
await report({
|
||||
case: {
|
||||
status: STATUS.STATUS_ACCEPTED,
|
||||
message: `Contest ${tdoc.title} finished`,
|
||||
time: Date.now() - start,
|
||||
memory: 0,
|
||||
score: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
for (const key in udict) udict[key] = udict[key] / 2 - 725;
|
||||
},
|
||||
hidden: false,
|
||||
base: 1500,
|
||||
},
|
||||
delta: {
|
||||
async run(domainIds, udict) {
|
||||
const dudocs = unionWith(
|
||||
await domain.getMultiUserInDomain(
|
||||
'', { domainId: { $in: domainIds }, rpdelta: { $exists: true } },
|
||||
).toArray(),
|
||||
(a, b) => a.uid === b.uid,
|
||||
);
|
||||
for (const dudoc of dudocs) udict[dudoc.uid] = dudoc.rpdelta;
|
||||
},
|
||||
hidden: true,
|
||||
base: 0,
|
||||
},
|
||||
};
|
||||
global.Hydro.model.rp = RpTypes;
|
||||
|
||||
export async function calcLevel(domainId: string, report: Function) {
|
||||
const filter = { rp: { $ne: 1500, $exists: true } };
|
||||
const filter = { rp: { $exists: true } };
|
||||
const ducnt = await domain.getMultiUserInDomain(domainId, filter).count();
|
||||
await domain.setMultiUserInDomain(domainId, {}, { level: 0, rank: null });
|
||||
if (!ducnt) return;
|
||||
@@ -122,50 +133,28 @@ export async function calcLevel(domainId: string, report: Function) {
|
||||
await bulk.execute();
|
||||
}
|
||||
|
||||
async function runInDomain(id: string, isSub: boolean, report: Function) {
|
||||
async function runInDomain(id: string, report: Function) {
|
||||
const info = await domain.getUnion(id);
|
||||
if (info) info.union.push(id);
|
||||
const udict: ND = {};
|
||||
const deltaudict: ND = {};
|
||||
const domainId = info ? { $in: info.union } : id;
|
||||
const dudocs = unionWith([
|
||||
...await domain.getMultiUserInDomain(id).toArray(),
|
||||
...await domain.getMultiUserInDomain('', { domainId }).toArray(),
|
||||
], (a, b) => a.uid === b.uid);
|
||||
for (const dudoc of dudocs) deltaudict[dudoc.uid] = dudoc.rpdelta;
|
||||
// TODO pagination
|
||||
const problems = await problem.getMulti('', { domainId, nSubmit: { $gt: 0 }, hidden: false }).toArray();
|
||||
if (problems.length) await report({ message: `Found ${problems.length} problems in ${id}` });
|
||||
for (const i in problems) {
|
||||
const pdoc = problems[i];
|
||||
await runProblem(pdoc, udict);
|
||||
if (!isSub) {
|
||||
await report({
|
||||
progress: Math.floor(((+i + 1) / problems.length) * 100),
|
||||
});
|
||||
if (info) info.union.unshift(id);
|
||||
const domainIds = info ? info.union : [id];
|
||||
const results: Record<keyof typeof RpTypes, ND> = {};
|
||||
const udict = new Proxy({}, { get: (self, key) => self[key] || 0 });
|
||||
for (const type in RpTypes) {
|
||||
results[type] = new Proxy({}, { get: (self, key) => self[key] || RpTypes[type].base });
|
||||
await RpTypes[type].run(domainIds, results[type], report);
|
||||
for (const uid in results[type]) {
|
||||
const udoc = await UserModel.getById(id, +uid);
|
||||
if (!udoc?.hasPriv(PRIV.PRIV_USER_PROFILE)) continue;
|
||||
await domain.updateUserInDomain(id, +uid, { $set: { [`rpInfo.${type}`]: results[type][uid] } });
|
||||
udict[+uid] += results[type][uid];
|
||||
}
|
||||
}
|
||||
const contests: Tdoc<30 | 60>[] = await contest.getMulti('', { domainId, rated: true })
|
||||
.toArray() as any;
|
||||
if (contests.length) await report({ message: `Found ${contests.length} contests in ${id}` });
|
||||
for (const i in contests) {
|
||||
const tdoc = contests[i];
|
||||
await runContest(tdoc, udict, report);
|
||||
if (!isSub) {
|
||||
await report({
|
||||
progress: Math.floor(((+i + 1) / contests.length) * 100),
|
||||
});
|
||||
}
|
||||
await domain.setMultiUserInDomain(id, {}, { rp: 0 });
|
||||
const bulk = db.collection('domain.user').initializeUnorderedBulkOp();
|
||||
for (const uid in udict) {
|
||||
bulk.find({ domainId: id, uid: +uid }).upsert().update({ $set: { rp: Math.max(0, udict[uid]) } });
|
||||
}
|
||||
await domain.setMultiUserInDomain(id, {}, { rp: 1500 });
|
||||
const tasks = [];
|
||||
async function update(uid: number, rp: number) {
|
||||
const udoc = await UserModel.getById(id, +uid);
|
||||
const $upd: any = { $set: { rp: Math.max(0, rp) } };
|
||||
if (udoc?.hasPriv(PRIV.PRIV_USER_PROFILE)) await domain.updateUserInDomain(id, +uid, $upd);
|
||||
}
|
||||
for (const uid in udict) tasks.push(update(+uid, udict[uid] + (deltaudict[uid] || 0)));
|
||||
await Promise.all(tasks);
|
||||
if (bulk.length) await bulk.execute();
|
||||
await calcLevel(id, report);
|
||||
}
|
||||
|
||||
@@ -175,7 +164,7 @@ export async function run({ domainId }, report: Function) {
|
||||
await report({ message: `Found ${domains.length} domains` });
|
||||
for (const i in domains) {
|
||||
const start = new Date().getTime();
|
||||
await runInDomain(domains[i]._id, true, report);
|
||||
await runInDomain(domains[i]._id, report);
|
||||
await report({
|
||||
case: {
|
||||
status: STATUS.STATUS_ACCEPTED,
|
||||
@@ -187,7 +176,7 @@ export async function run({ domainId }, report: Function) {
|
||||
progress: Math.floor(((+i + 1) / domains.length) * 100),
|
||||
});
|
||||
}
|
||||
} else await runInDomain(domainId, false, report);
|
||||
} else await runInDomain(domainId, report);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ problem.import.problem_description: '## Description'
|
||||
ranking: Ranking
|
||||
record_detail: Record Detail
|
||||
record_main: Record
|
||||
rpDetailText_contest: Contest
|
||||
rpDetailText_problem: Problem
|
||||
setting_customize: Customize
|
||||
setting_display: Display Preference
|
||||
setting_function: Function
|
||||
|
||||
@@ -561,6 +561,7 @@ Please select at least one file to perform this operation.: 请选择至少一
|
||||
Please select at least one role to perform this operation.: 请选择至少一个角色来进行操作。
|
||||
Please select at least one user to perform this operation.: 请选择至少一个用户来进行操作。
|
||||
Preference Settings: 偏好设置
|
||||
Preparing Upload...: 准备上传...
|
||||
Pretest Data: 自测数据
|
||||
Pretest Your Code: 使用自定义数据测试
|
||||
Pretest: 自测
|
||||
@@ -659,6 +660,8 @@ Role name can only contains numbers, letters and underscores.: 角色名称只
|
||||
Role: 角色
|
||||
Roles: 角色
|
||||
RP Algorithm: RP 算法
|
||||
rpDetailText_contest: 比赛
|
||||
rpDetailText_problem: 题目
|
||||
Rule: 规则
|
||||
Run Pretest: 运行自测
|
||||
Run: 运行
|
||||
@@ -809,12 +812,14 @@ Unstar: 取消收藏
|
||||
Update Permission: 更新权限
|
||||
Update Settings: 更新设置
|
||||
Update: 更新
|
||||
Upload Failed: 上传失败
|
||||
Upload File: 上传文件
|
||||
Upload Problem: 上传题目
|
||||
Upload: 上传
|
||||
Uploaded By: 上传者
|
||||
Uploading files...: 正在上传文件...
|
||||
Uploading... ({0}%): 正在上传... ({0}%)
|
||||
Uploading...: 正在上传...
|
||||
Upvote: 好评
|
||||
Usage exceeded.: 用量超限。
|
||||
Use admin specificed: 由管理员指定
|
||||
@@ -827,9 +832,6 @@ User ID: 用户 ID
|
||||
User will no longer be allowed to join the domain after expiration.: 过期后,用户将不能加入此域。
|
||||
user_detail: 用户
|
||||
user_login: 登录
|
||||
Uploading...: 正在上传...
|
||||
Upload Failed: 上传失败
|
||||
Preparing Upload...: 准备上传...
|
||||
user_logout: 登出
|
||||
user_lostpass: 忘记密码
|
||||
user_register_with_code: 注册
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/ui-default",
|
||||
"version": "4.33.1",
|
||||
"version": "4.33.2",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "hydro.js",
|
||||
|
||||
@@ -7,17 +7,21 @@
|
||||
width: rem(220px)
|
||||
|
||||
.col--rp
|
||||
width: rem(80px)
|
||||
width: rem(60px)
|
||||
text-align: right
|
||||
|
||||
.col--detail
|
||||
width: rem(50px)
|
||||
text-align: center
|
||||
|
||||
.col--ac
|
||||
width: rem(70px)
|
||||
text-align: right
|
||||
width: rem(50px)
|
||||
text-align: center
|
||||
|
||||
.col--bio
|
||||
height: 36px !important;
|
||||
display: block;
|
||||
|
||||
+mobile()
|
||||
.col--bio
|
||||
.col--bio, .col--detail
|
||||
display: none
|
||||
@@ -15,6 +15,11 @@
|
||||
<col class="col--rank">
|
||||
<col class="col--user">
|
||||
<col class="col--rp">
|
||||
{% for key, def in model.rp %}
|
||||
{% if not def.hidden %}
|
||||
<col class="col--detail rp-{{ key }}">
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<col class="col--ac">
|
||||
<col class="col--bio">
|
||||
</colgroup>
|
||||
@@ -23,6 +28,11 @@
|
||||
<th class="col--rank">{{ _('Rank') }}</th>
|
||||
<th class="col--user">{{ _('Username') }}</th>
|
||||
<th class="col--rp">{{ _('RP') }}</th>
|
||||
{% for key, def in model.rp %}
|
||||
{% if not def.hidden %}
|
||||
<th class="col--detail rp-{{ key }}">{{ _('rpDetailText_' + key) }}</th>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<th class="col--ac">{{ _('Accept') }}</th>
|
||||
<th class="col--bio">{{ _('Bio') }}</th>
|
||||
</tr>
|
||||
@@ -33,6 +43,11 @@
|
||||
<td class="col--rank">{{ (page - 1) * 100 + loop.index }}</td>
|
||||
<td class="col--user">{{ user.render_inline(udoc) }}</td>
|
||||
<td class="col--rp">{{ udoc.rp|default(1500)|round(0) }}</td>
|
||||
{% for key, def in model.rp %}
|
||||
{% if not def.hidden %}
|
||||
<td class="col--detail rp-{{ key }}">{{ udoc.rpInfo[key]|round if udoc.rpInfo[key] else '-' }}</td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<td class="col--ac">{{ udoc.nAccept|default(0) }}</td>
|
||||
<td class="col--bio">{{ udoc.bio|default('')|truncate(64, true)|markdownInline|safe }}</td>
|
||||
</tr>
|
||||
|
||||
Reference in new issue
Block a user