UOJ Predictor

Plugin to calculate predicted rating changes of UOJ-like Online Judges.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         UOJ Predictor
// @namespace    http://tampermonkey.net/
// @version      0.6
// @description  Plugin to calculate predicted rating changes of UOJ-like Online Judges.
// @author       tiger2005
// @match        *://zhengruioi.com/contest/*/standings*
// @match        *://uoj.ac/contest/*/standings*
// @match        *://www.zhengruioi.com/contest/*/standings*
// @match        *://www.uoj.ac/contest/*/standings*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';
    // transformed from https://github.com/vfleaking/uoj/blob/9f1302c774f2499af0dc52d3faa7dd7404d03b13/uoj/1/app/uoj-contest-lib.php
	function getColOfRating(rating) {
		if (rating < 1500) {
			var H = 300 - (1500 - 850) * 300 / 1650, S = 30 + (1500 - 850) * 70 / 1650, V = 50 + (1500 - 850) * 50 / 1650;
			if (rating < 300) rating = 300;
			var k = (rating - 300) / 1200;
			return ColorConverter.toStr(ColorConverter.toRGB(new HSV(H + (300 - H) * (1 - k), 30 + (S - 30) * k, 50 + (V - 50) * k)));
		}
		if (rating > 2500) {
			rating = 2500;
		}
		return ColorConverter.toStr(ColorConverter.toRGB(new HSV(300 - (rating - 850) * 300 / 1650, 30 + (rating - 850) * 70 / 1650, 50 + (rating - 850) * 50 / 1650)));
	}
    function calcPredictedRatingChanges(K = 400){
        var delta = 500;
        var n = standings.length;
        var rating = [];
        var i, j;
        for(i = 0; i < n; i ++)
            rating.push(standings[i][2][1]);
        var rank = [];
        var foot = [];
        for(i = 0; i < n; ){
            j = i;
            while(j + 1 < n && standings[j+1][3] == standings[j][3])
                ++ j;
            var our_rk = 0.5 * ((i+1) + (j+1));
            while(i <= j){
                rank.push(our_rk);
                foot.push(n - rank[i]);
                i ++;
            }
        }
        var weight = [];
        for(i = 0; i < n; i ++)
            weight.push(Math.pow(7, rating[i] / delta));
        var exp = [];
        for(i = 0; i < n; i ++)
            exp.push(0);
        for(i = 0; i < n; i ++)
            for(j = 0; j < n; j ++)
                if(j != i)
                    exp[i] += weight[i] / (weight[i] + weight[j]);
        var new_rating = [];
        for(i = 0; i < n; i ++)
            new_rating.push(rating[i] + Math.ceil(K * (foot[i] - exp[i]) / (n - 1)));
        for(i = n - 1; i >= 0; i --){
            if(i + 1 < n && standings[i][3] != standings[i+1][3])
                break;
            if(new_rating[i] > rating[i])
                new_rating[i] = rating[i];
        }
        for(i = 0; i < n; i ++)
            if(new_rating[i] < 0)
                new_rating[i] = 0;
        return new_rating;
    }
    var max_rating_changes = localStorage.getItem("_MAX_RATING_CHANGES");
    if(max_rating_changes == undefined){
        localStorage.setItem("_MAX_RATING_CHANGES", 400);
        max_rating_changes = 400;
    }
    else
        max_rating_changes = Number(max_rating_changes);
    function getUsernameTable(){
        var users = {};
        var n = standings.length;
        for(var i = 0; i < n; i ++)
            users[standings[i][2][0]] = [i, standings[i][2][1]];
        return users;
    }
    var predicted_rating_changes = calcPredictedRatingChanges(max_rating_changes);
    var username_table = getUsernameTable();
    var rating_changes_colors = ["rgb(0, 204, 0)", "rgb(102, 102, 102)", "rgb(204, 0, 0)"];
    function changeMaxRatingChanges(){
        var str = prompt("Insert max rating changes, ranged [0, 1000]");
        var result = Number(str);
        if(typeof(result) != "number" || isNaN(result) || result < 0 || result > 1000)
            alert("Invalid input!");
        else{
            var st = $("#standings > .table-responsive > table");
            st.find("thead > tr > th:last-child").remove();
            st.find("tbody > tr > td:last-child").remove();
			st.find("thead > tr > th:last-child").remove();
            st.find("tbody > tr > td:last-child").remove();
            max_rating_changes = Math.floor(result);
            localStorage.setItem("_MAX_RATING_CHANGES", max_rating_changes);
            predicted_rating_changes = calcPredictedRatingChanges(max_rating_changes);
            displayRatingChanges();
        }
    }
    function formatAsUsername(str){
        var ret = "";
        for(var i=0; i<str.length; i++)
            if(str[i].charCodeAt() > 32 && str[i].charCodeAt() < 128)
                ret += str[i];
        return ret;
    }
    function displayRatingChanges(){
        var st = $("#standings > .table-responsive > table");
        if(max_rating_changes == 0)
            st.find("thead > tr").append(`<th style="width: 6em"><a class="changeMaxRatingChanges" style="cursor: pointer">Unrated</a></th>`)
        else
            st.find("thead > tr").append(`<th style="width: 6em"><a class="changeMaxRatingChanges" style="cursor: pointer">Delta<br/>(max. = ${max_rating_changes})</a></th>`);
		st.find("thead > tr").append(`<th style="width: 6em"><a>New rating</a></th>`);
        st.find("tbody > tr").each(function(){
            var username = $(this).children().eq(1).find("a").text();
            username = formatAsUsername(username);
            var rating_changes = predicted_rating_changes[username_table[username][0]] - username_table[username][1];
            var color = "", content = "";
            if(rating_changes > 0)
                color = rating_changes_colors[0], content = '+' + rating_changes;
            else if(rating_changes == 0)
                color = rating_changes_colors[1], content = '' + rating_changes;
            else
                color = rating_changes_colors[2], content = '' + rating_changes;
            $(this).append(`<td><div><span style='color: ${color}'>${content}</span></div></td>`);
			$(this).append(`<td><div><span><span style='color: ${getColOfRating(username_table[username][1])}'>${username_table[username][1]}</span> → <span style='color: ${getColOfRating(predicted_rating_changes[username_table[username][0]])}'>${predicted_rating_changes[username_table[username][0]]}</span></span></div></td>`);
        })
        $(".changeMaxRatingChanges").click(function(){
            changeMaxRatingChanges();
        })
    }
    $.fn.long_table = function(data, cur_page, header_row, get_row_str, config) {
        return this.each(function() {
            var table_div = this;

            $(table_div).html('');

            var page_len = config.page_len != undefined ? config.page_len : 10;

            if (!config.echo_full) {
                var n_rows = data.length;
                var n_pages = Math.max(Math.ceil(n_rows / page_len), 1);
                if (cur_page == undefined) {
                    cur_page = 1;
                }
                if (cur_page < 1) {
                    cur_page = 1;
                } else if (cur_page > n_pages) {
                    cur_page = n_pages;
                }
                var cur_start = (cur_page - 1) * page_len;
            } else {
                var n_rows = data.length;
                var n_pages = 1;
                cur_page = 1;
                var cur_start = (cur_page - 1) * page_len;
            }

            var div_classes = config.div_classes != undefined ? config.div_classes : ['table-responsive'];
            var table_classes = config.table_classes != undefined ? config.table_classes : ['table', 'table-bordered', 'table-hover', 'table-striped', 'table-text-center'];

            var now_cnt = 0;
            var tbody = $('<tbody />')
            for (var i = 0; i < page_len && cur_start + i < n_rows; i++) {
                now_cnt++;
                if (config.get_row_index) {
                    tbody.append(get_row_str(data[cur_start + i], cur_start + i));
                } else {
                    tbody.append(get_row_str(data[cur_start + i]));
                }
            }
            if (now_cnt == 0) {
                tbody.append('<tr><td colspan="233">无</td></tr>');
            }

            $(table_div).append(
                $('<div class="' + div_classes.join(' ') + '" />').append(
                    $('<table class="' + table_classes.join(' ') + '" />').append(
                        $('<thead>' + header_row + '</thead>')
                    ).append(
                        tbody
                    )
                )
            );

            if (config.print_after_table != undefined) {
                $(table_div).append(config.print_after_table());
            }

            var get_page_li = function(p, h) {
                if (p == -1) {
                    return $('<li></li>').addClass('disabled').append($('<a></a>').append(h));
                }

                var li = $('<li></li>');
                if (p == cur_page) {
                    li.addClass('active');
                }
                li.append(
                    $('<a></a>').attr('href', '#' + table_div.id).append(h).click(function(e) {
                        if (config.prevent_focus_on_click) {
                            e.preventDefault();
                        }
                        $(table_div).long_table(data, p, header_row, get_row_str, config);
                    })
                );
                return li;
            };

            if (n_pages > 1) {
                var pagination = $('<ul class="pagination top-buffer-no bot-buffer-sm"></ul>');
                if (cur_page > 1) {
                    pagination.append(get_page_li(cur_page - 1, '<span class="glyphicon glyphicon glyphicon-backward"></span>'));
                } else {
                    pagination.append(get_page_li(-1, '<span class="glyphicon glyphicon glyphicon-backward"></span>'));
                }
                var max_extend = config.max_extend != undefined ? config.max_extend : 5;
                for (var i = Math.max(cur_page - max_extend, 1); i <= Math.min(cur_page + max_extend, n_pages); i++) {
                    pagination.append(get_page_li(i, i.toString()));
                }
                if (cur_page < n_pages) {
                    pagination.append(get_page_li(cur_page + 1, '<span class="glyphicon glyphicon glyphicon-forward"></span>'));
                } else {
                    pagination.append(get_page_li(-1, '<span class="glyphicon glyphicon glyphicon-forward"></span>'));
                }
                $(table_div).append($('<div class="text-center"></div>').append(pagination));
            }
            displayRatingChanges();
        });
    };
    displayRatingChanges();
})();