From 9e86d47d1976c2f14c05134d689cccf3b68eb0c0 Mon Sep 17 00:00:00 2001 From: undefined Date: Tue, 8 Mar 2022 00:33:41 +0800 Subject: [PATCH] core: update rp method --- packages/hydrooj/package.json | 2 +- packages/hydrooj/src/interface.ts | 1 + packages/hydrooj/src/model/contest.ts | 2 +- packages/hydrooj/src/model/setting.ts | 3 +- packages/hydrooj/src/script/rating.ts | 211 ++++++++++---------- packages/ui-default/locales/en.yaml | 2 + packages/ui-default/locales/zh.yaml | 8 +- packages/ui-default/package.json | 2 +- packages/ui-default/pages/ranking.page.styl | 12 +- packages/ui-default/templates/ranking.html | 15 ++ 10 files changed, 136 insertions(+), 122 deletions(-) diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 1b8d61662..7369870a1 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "3.7.2", + "version": "3.8.0", "bin": "bin/hydrooj.js", "main": "src/loader", "module": "src/loader", diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 14486c7bf..63d602a24 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -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 { diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index 0797b5ec1..f43c0c1c9 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -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); }, }); diff --git a/packages/hydrooj/src/model/setting.ts b/packages/hydrooj/src/model/setting.ts index 647e71020..5fe104569 100644 --- a/packages/hydrooj/src/model/setting.ts +++ b/packages/hydrooj/src/model/setting.ts @@ -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), diff --git a/packages/hydrooj/src/script/rating.ts b/packages/hydrooj/src/script/rating.ts index 23ba44596..e3f5e03c6 100644 --- a/packages/hydrooj/src/script/rating.ts +++ b/packages/hydrooj/src/script/rating.ts @@ -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; -async function runProblem(pdoc: ProblemDoc, udict: ND): Promise; -async function runProblem(domainId: string, pid: number, udict: ND): Promise; -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; + hidden: boolean; + base: number; } -async function runContest(tdoc: Tdoc<30 | 60>, udict: ND, report: Function): Promise; -async function runContest( - domainId: string, tid: ObjectID, udict: ND, report: Function -): Promise; -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 = { + 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 = {}; + 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; } diff --git a/packages/ui-default/locales/en.yaml b/packages/ui-default/locales/en.yaml index cb3ed5383..f7c73a83f 100644 --- a/packages/ui-default/locales/en.yaml +++ b/packages/ui-default/locales/en.yaml @@ -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 diff --git a/packages/ui-default/locales/zh.yaml b/packages/ui-default/locales/zh.yaml index c4d15aeea..b3059c060 100644 --- a/packages/ui-default/locales/zh.yaml +++ b/packages/ui-default/locales/zh.yaml @@ -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: 注册 diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 1a8c8a2b1..3851f1de3 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.33.1", + "version": "4.33.2", "author": "undefined ", "license": "AGPL-3.0", "main": "hydro.js", diff --git a/packages/ui-default/pages/ranking.page.styl b/packages/ui-default/pages/ranking.page.styl index fca4657d3..0c4a9beb9 100644 --- a/packages/ui-default/pages/ranking.page.styl +++ b/packages/ui-default/pages/ranking.page.styl @@ -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 diff --git a/packages/ui-default/templates/ranking.html b/packages/ui-default/templates/ranking.html index ed48580ed..fd9a6b164 100644 --- a/packages/ui-default/templates/ranking.html +++ b/packages/ui-default/templates/ranking.html @@ -15,6 +15,11 @@ + {% for key, def in model.rp %} + {% if not def.hidden %} + + {% endif %} + {% endfor %} @@ -23,6 +28,11 @@ {{ _('Rank') }} {{ _('Username') }} {{ _('RP') }} + {% for key, def in model.rp %} + {% if not def.hidden %} + {{ _('rpDetailText_' + key) }} + {% endif %} + {% endfor %} {{ _('Accept') }} {{ _('Bio') }} @@ -33,6 +43,11 @@ {{ (page - 1) * 100 + loop.index }} {{ user.render_inline(udoc) }} {{ udoc.rp|default(1500)|round(0) }} + {% for key, def in model.rp %} + {% if not def.hidden %} + {{ udoc.rpInfo[key]|round if udoc.rpInfo[key] else '-' }} + {% endif %} + {% endfor %} {{ udoc.nAccept|default(0) }} {{ udoc.bio|default('')|truncate(64, true)|markdownInline|safe }}