diff --git a/libs/rating.js b/libs/rating.js new file mode 100644 index 0000000..cc7e95b --- /dev/null +++ b/libs/rating.js @@ -0,0 +1,77 @@ +// Code by Dicint + +const util = require('util'); +const _ = require('lodash'); + +function getEloWinProbability(ra, rb) { + return 1.0 / (1 + Math.pow(10, (rb - ra) / 400.0)); +} + +// 传入具体某个人的情况 +function getContestantSeed(contestantIndex, allContestants) { + let seed = 1; + let rating = allContestants[contestantIndex].currentRating; + for (let i = 0; i < allContestants.length; i++) { + if (contestantIndex != i) { + seed += getEloWinProbability(allContestants[i].currentRating, rating); + } + } + return seed; +} + +function getRatingSeed(rating, allContestants) { + return 1 + _.sum(allContestants.map(c => getEloWinProbability(c.currentRating, rating))); +} + +function getAverageRank(contestant, allContestants) { + const realRank = allContestants[contestant].rank; + const expectedRank = getContestantSeed(contestant, allContestants); + const average = Math.sqrt(realRank * expectedRank); + + return average; +} + +function getRatingToRank(contestantIndex, allContestants) { + let averageRank = getAverageRank(contestantIndex, allContestants); + + let left = 1;// contestant.getPrevRating() - 2 * minDelta; + let right = 8000;// contestant.getPrevRating() + 2 * maxDelta; + + while (right - left > 1) { + const mid = (left + right) / 2; + const seed = getRatingSeed(mid, allContestants); + if (seed < averageRank) { + right = mid; + } else { + left = mid; + } + } + return left; +} + +function calculateDeltas(allContestants) { + let deltas = []; + const numberOfContestants = allContestants.length; + for (let i = 0; i < allContestants.length; i++) { + const expR = getRatingToRank(i, allContestants); + deltas[i] = ((expR - allContestants[i].currentRating) / 2); + } + + // Total sum should not be more than zero. + const deltaSum = _.sum(deltas); + const inc = -deltaSum / numberOfContestants - 1; + deltas = deltas.map(d => d + inc); + + // Sum of top-4*sqrt should be adjusted to zero. + const zeroSumCount = Math.min(Math.trunc(4 * Math.round(Math.sqrt(numberOfContestants))), numberOfContestants); + const deltaSum2 = _.sum(deltas.slice(0, zeroSumCount)); + const inc2 = Math.min(Math.max(-deltaSum2 / zeroSumCount, -10), 0); + deltas = deltas.map(d => d + inc2); + + return deltas; +} + +module.exports = function(allContestants) { + const deltas = calculateDeltas(allContestants); + return allContestants.map((contestant, i) => ({ user: contestant.user, currentRating: contestant.currentRating + deltas[i] })); +} \ No newline at end of file diff --git a/modules/admin.js b/modules/admin.js index e448c32..92fdd69 100644 --- a/modules/admin.js +++ b/modules/admin.js @@ -23,6 +23,10 @@ let Article = syzoj.model('article'); let Contest = syzoj.model('contest'); let User = syzoj.model('user'); let UserPrivilege = syzoj.model('user_privilege'); +const RatingCalculation = syzoj.model('rating_calculation'); +const RatingHistory = syzoj.model('rating_history'); +let ContestPlayer = syzoj.model('contest_player'); +const calcRating = require('../libs/rating'); let db = syzoj.db; @@ -198,6 +202,86 @@ app.post('/admin/privilege', async (req, res) => { } }); +app.get('/admin/rating', async (req, res) => { + try { + if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。'); + const contests = await Contest.query(null, {}, [['start_time', 'desc']]); + const calcs = await RatingCalculation.query(null, {}, [['id', 'desc']]); + const util = require('util'); + for (const calc of calcs) await calc.loadRelationships(); + + res.render('admin_rating', { + contests: contests, + calcs: calcs + }); + } catch (e) { + syzoj.log(e); + res.render('error', { + err: e + }) + } +}); + +app.post('/admin/rating/add', async (req, res) => { + try { + if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。'); + const contest = await Contest.fromID(req.body.contest); + if (!contest) throw new ErrorMessage('无此比赛'); + + await contest.loadRelationships(); + const newcalc = await RatingCalculation.create(contest.id); + await newcalc.save(); + + if (!contest.ranklist || contest.ranklist.ranklist.player_num <= 1) { + throw new ErrorMessage("比赛人数太少。"); + } + + const players = []; + for (let i = 1; i <= contest.ranklist.ranklist.player_num; i++) { + const user = await User.fromID((await ContestPlayer.fromID(contest.ranklist.ranklist[i])).user_id); + players.push({ + user: user, + rank: i, + currentRating: user.rating + }); + } + const newRating = calcRating(players); + for (let i = 0; i < newRating.length; i++) { + const user = newRating[i].user; + user.rating = newRating[i].currentRating; + await user.save(); + const newHistory = await RatingHistory.create(newcalc.id, user.id, user.rating); + await newHistory.save(); + } + + res.redirect(syzoj.utils.makeUrl(['admin', 'rating'])); + } catch (e) { + syzoj.log(e); + res.render('error', { + err: e + }); + } +}); + +app.post('/admin/rating/delete', async (req, res) => { + try { + if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。'); + const calcList = await RatingCalculation.query(null, { id: { $gte: req.body.calc_id } }, [['id', 'desc']]); + if (calcList.length === 0) throw new ErrorMessage('ID 不正确'); + + for (let i = 0; i < calcList.length; i++) { + await calcList[i].delete(); + } + + res.redirect(syzoj.utils.makeUrl(['admin', 'rating'])); + } catch (e) { + syzoj.log(e); + res.render('error', { + err: e + }); + } +}); + app.get('/admin/other', async (req, res) => { try { if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。'); @@ -233,7 +317,7 @@ app.post('/admin/other', async (req, res) => { if (req.body.type === 'reset_count') { const problems = await Problem.query(); - for(const p of problems) { + for (const p of problems) { await p.resetSubmissionCount(); } } else { @@ -248,7 +332,6 @@ app.post('/admin/other', async (req, res) => { }) } }); - app.post('/admin/rejudge', async (req, res) => { try { if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。'); diff --git a/views/admin_header.ejs b/views/admin_header.ejs index 23bcad1..b78fae7 100644 --- a/views/admin_header.ejs +++ b/views/admin_header.ejs @@ -5,6 +5,7 @@ let items = { privilege: '权限管理', rejudge: '一键重测', links: '友链管理', + rating: 'Rating 管理', raw: '配置文件', other: '其他操作' }; diff --git a/views/admin_rating.ejs b/views/admin_rating.ejs new file mode 100644 index 0000000..b50d0e9 --- /dev/null +++ b/views/admin_rating.ejs @@ -0,0 +1,34 @@ +<% this.adminPage = 'rating'; %> +<% include admin_header %> +
+
+ + +
+ +
+ +注意:如果删除一个比赛的 Rating,则该比赛之上的所有比赛也将被删除,Rating 将还原至该比赛之前的状态! +
+ +
+ <% for (const calc of calcs) { %> +
+ <%= calc.contest.title %> + +
+ <% } %> +
+ +
+<% include admin_footer %> + \ No newline at end of file