// ==UserScript==
// @name ac-predictor
// @namespace http://ac-predictor.azurewebsites.net/
// @version 0.1.6
// @description コンテスト中にAtCoderのパフォーマンスを予測します
// @author keymoon
// @license MIT
// @homepage https://github.com/key-moon/ac-predictor
// @supportURL https://github.com/key-moon/ac-predictor/issues
// @match https://beta.atcoder.jp/*
// @downloadURL none
// ==/UserScript==
//NameSpace
SideMenu = {};
//共有データセット(HistoryとかStandingsとか)
SideMenu.Datas = {};
//共有データセットそれぞれをUpdateする関数を入れておく
SideMenu.Datas.Update = {}
//Datas.Update内に関数を追加
//History
SideMenu.Datas.History = null;
SideMenu.Datas.Update.History = (() => {
var d = $.Deferred();
try {
$.ajax({
url: `https://beta.atcoder.jp/users/${userScreenName}/history/json`,
type: "GET",
dataType: "json"
}).done(history => {
SideMenu.Datas.History = history;
d.resolve();
}).fail(() => { d.reject(); });
}
catch {
d.reject();
}
return d.promise();
});
//Standings
SideMenu.Datas.Standings = null;
SideMenu.Datas.Update.Standings = (() => {
var d = $.Deferred();
try {
$.ajax({
url: `https://beta.atcoder.jp/contests/${contestScreenName}/standings/json`,
type: "GET",
dataType: "json"
}).done(standings => {
SideMenu.Datas.Standings = standings;
d.resolve();
}).fail(() => { d.reject(); });
}
catch{
d.reject();
}
return d.promise();
});
//APerfs
SideMenu.Datas.APerfs = null;
SideMenu.Datas.Update.APerfs = (() => {
var d = $.Deferred();
try {
$.ajax({
url: `https://ac-predictor.azurewebsites.net/api/aperfs/${contestScreenName}`,
type: "GET",
dataType: "json"
}).done(aperfs => {
SideMenu.Datas.APerfs = aperfs
d.resolve();
}).fail(() => { d.reject(); });
}
catch {
d.reject();
}
return d.promise();
});
//ライブラリを追加するやつ
SideMenu.appendLibrary = function (source) {
$('head').append(``);
};
SideMenu.appendLibrary("https://koba-e964.github.io/atcoder-rating-estimator/atcoder_rating.js");
//サイドメニュー追加(将来仕様変更が起きる可能性大)
SideMenu.appendToSideMenu = async function (match, title, elemFunc) {
var defferd = $.Deferred();
try {
if (!match.test(location.href)) return;
//アコーディオンメニュー
var dom =
`
`
$('#sidemenu').append(dom);
var contents = $('.menu-content');
var contentElem = contents[contents.length - 1];
$(contentElem).parents('.menu-box').css('height', contentElem.scrollHeight)
defferd.resolve();
}
catch (e) {
console.error(e);
defferd.reject();
}
return defferd.promise();
};
//サイドメニューを生成
(function () {
var menuWidth = 350
var keyWidth = 50
var speed = 150
var sideMenuScript =
``
var sideMenuStyle =
``
var ratingScript =
``;
$('#main-div').append(``);
})();
//サイドメニュー要素の入れ物
SideMenu.Elements = {};
SideMenu.ViewOrder = ["Predictor","Estimator"];
//Estimator
SideMenu.Elements.Estimator = (async () => {
await SideMenu.appendToSideMenu(/beta.atcoder.jp/,'Estimator',getElem);
async function getElem() {
$("#estimator-input").val(localStorage.getItem("sidemenu_estimator_value"));
if (!SideMenu.Datas.History) await SideMenu.Datas.Update.History();
var js =
`(() => {
var estimator_state = localStorage.getItem("sidemenu_estimator_state");
updateInputs();
\$("#estimator-input").keyup(updateInputs);
\$("#estimator-toggle").click(function () {
if (estimator_state === 0) {
\$("#estimator-input-desc").text("パフォーマンス")
\$("#estimator-res-desc").text("到達レーティング")
estimator_state = 1;
}
else {
\$("#estimator-input-desc").text("目標レーティング")
\$("#estimator-res-desc").text("必要パフォーマンス")
estimator_state = 0;
}
\$("#estimator-input").val(\$("#estimator-res").val());
updateInputs();
updateLocalStorage()
updateTweetBtn()
})
function updateInputs () {
var input = \$("#estimator-input").val();
if (!isFinite(input)) {
displayAlert("数字ではありません")
return;
}
var history = SideMenu.Datas.History.filter(x => x.IsRated)
history.sort(function (a, b) {
if (a.EndTime < b.EndTime) return 1;
if (a.EndTime > b.EndTime) return -1;
return 0;
})
history = history.map(x => x.InnerPerformance)
var input = parseInt(input, 10)
var res = -1;
if (estimator_state === 0) {
// binary search
var goal_rating = unpositivize_rating(input)
var lo = -10000.0;
var hi = 10000.0;
for (var i = 0; i < 100; ++i) {
var mid = (hi + lo) / 2;
var r = calc_rating([mid].concat(history));
if (r >= goal_rating) {
hi = mid;
} else {
lo = mid;
}
}
res = (hi + lo) / 2;
}
else {
res = calc_rating([input].concat(history));
}
res = Math.round(res * 100) / 100
\$("#estimator-res").val(res)
updateLocalStorage()
updateTweetBtn()
}
function updateLocalStorage() {
localStorage.setItem("sidemenu_estimator_state", estimator_state);
localStorage.setItem("sidemenu_estimator_value", \$("#estimator-input").val());
}
function updateTweetBtn() {
var tweetStr =
\`AtCoderのハンドルネーム: \${userScreenName}%0A
\${estimator_state == 0 ? "目標レーティング" : "パフォーマンス"}: \${\$("#estimator-input").val()}%0A
\${estimator_state == 0 ? "必要パフォーマンス" : "到達レーティング"}: \${\$("#estimator-res").val()}\`
\$('#estimator-tweet').attr("href", \`https://twitter.com/intent/tweet?text=\${tweetStr}\`)
}
function displayAlert(message) {
var alertDiv = document.createElement('div')
alertDiv.setAttribute("role", "alert")
alertDiv.setAttribute("class", "alert alert-warning alert-dismissible")
var closeButton = document.createElement('button')
closeButton.setAttribute("type", "button")
closeButton.setAttribute("class", "close")
closeButton.setAttribute("data-dismiss", "alert")
closeButton.setAttribute("aria-label", "閉じる")
var closeSpan = document.createElement('span')
closeSpan.setAttribute("aria-hidden", "true")
closeSpan.textContent = "×"
closeButton.appendChild(closeSpan)
var messageContent = document.createTextNode(message)
alertDiv.appendChild(closeButton)
alertDiv.appendChild(messageContent)
\$("#estimator-alert").append(alertDiv)
}
})();`;
var style =
``;
var dom =
`
`;
return `${dom}
`;
}
});
//Predictor
SideMenu.Elements.Predictor = (async () => {
await SideMenu.appendToSideMenu(/beta.atcoder.jp\/contests\//,'Predictor',getElem);
async function getElem() {
//NameSpace
SideMenu.Predictor = {};
var maxDic =
[
[/^abc\d{3}$/, 1600],
[/^arc\d{3}$/, 3200],
[/^agc\d{3}$/, 8192],
[/^apc\d{3}$/, 8192],
[/^cf\d{2}-final-open$/, 8192],
[/^soundhound2018-summer-qual$/, 2400],
[/.*/, -1]
];
SideMenu.Predictor.maxPerf = maxDic.filter(x => x[0].exec(contestScreenName))[0][1];
if (!SideMenu.Datas.History) await SideMenu.Datas.Update.History().done(() => { isDone = true });
var js =
`(() => {
if (!startTime.isBefore()) {
\$("#predictor-input-rank").attr("disabled", "")
\$("#predictor-input-perf").attr("disabled", "")
\$("#predictor-input-rate").attr("disabled", "")
\$("#predictor-reload").attr("disabled", "")
\$("#predictor-current").attr("disabled", "")
\$("#predictor-tweet").attr("disabled", "")
\$("#predictor-alert").html("");
}
else {
LoadAPerfs()
if(!endTime.isBefore()) var loadTimer = setInterval(LoadAPerfs, 30000)
}
\$('[data-toggle="tooltip"]').tooltip()
function UpdatePredictor(rank,perf,rate) {
\$("#predictor-input-rank").val(round(rank))
\$("#predictor-input-perf").val(round(perf))
\$("#predictor-input-rate").val(round(rate))
updatePredictorTweetBtn()
function round(val) {
return Math.round(val * 100) / 100;
}
}
function UpdatePredictorFromRank(rank) {
var perf = getPerf(rank)
var rate = getRate(perf)
lastUpdated = 0
UpdatePredictor(rank,perf,rate)
}
function UpdatePredictorFromPerf(perf) {
var upper = 16384
var lower = 0
while(upper - lower > 0.125) {
if (perf > getPerf(lower + (upper - lower) / 2)) upper -= (upper - lower) / 2
else lower += (upper - lower) / 2
}
lastUpdated = 1
var rank = lower + (upper - lower) / 2;
var rate = getRate(perf)
UpdatePredictor(rank,perf,rate)
}
function UpdatePredictorFromRate(rate) {
var upper = 16384
var lower = 0
while(upper - lower > 0.125) {
if (rate < getRate(lower + (upper - lower) / 2)) upper -= (upper - lower) / 2
else lower += (upper - lower) / 2
}
lastUpdated = 2
var perf = lower + (upper - lower) / 2;
upper = 16384
lower = 0
while(upper - lower > 0.125) {
if (perf > getPerf(lower + (upper - lower) / 2)) upper -= (upper - lower) / 2
else lower += (upper - lower) / 2
}
var rank = lower + (upper - lower) / 2;
UpdatePredictor(rank,perf,rate)
}
function getRate(perf) {
return positivize_rating(calc_rating(SideMenu.Datas.History.filter(x => x.IsRated).map(x => x.Performance).concat(perf).reverse()));
}
function getPerf(rank) {
var upper = 8192
var lower = -8192
while (upper - lower > 0.5) {
if (rank - 0.5 > calcPerf(lower + (upper - lower) / 2)) upper -= (upper - lower) / 2
else lower += (upper - lower) / 2
}
var innerPerf = Math.round(lower + (upper - lower) / 2)
return Math.min(innerPerf, SideMenu.Predictor.maxPerf)
function calcPerf(X) {
var res = 0;
activePerf.forEach(function (APerf) {
res += 1.0 / (1.0 + Math.pow(6.0, ((X - APerf) / 400.0)))
})
return res;
}
}
\$('#predictor-current').click(function () {
//自分の順位を確認
var myRank = 0;
var tiedList = []
var lastRank = 0;
var rank = 1;
var isContainedMe = false;
//全員回して自分が出てきたら順位更新フラグを立てる
SideMenu.Datas.Standings.StandingsData.forEach(function (element) {
if (!element.IsRated || element.TotalResult.Count === 0) return;
if (lastRank !== element.Rank) {
if (isContainedMe) {
myRank = rank + (tiedList.length - 1) / 2;
isContainedMe = false;
}
rank += tiedList.length;
tiedList = []
}
if (isContainedMe) {
myRank = rank + (tiedList.length - 1) / 2;
isContainedMe = false;
}
if(userScreenName == element.UserScreenName) isContainedMe = true;
tiedList.push(element)
lastRank = element.Rank;
})
//存在しなかったら空欄
if(myRank === 0) {
UpdatePredictor("","","")
}
else {
UpdatePredictorFromRank(myRank)
}
})
function LoadStandings() {
SideMenu.Datas.Update.Standings()
.done(() => {
CalcActivePerf()
})
}
function CalcActivePerf() {
activePerf = []
//Perf計算時に使うパフォ(Ratedオンリー)
SideMenu.Datas.Standings.StandingsData.forEach(function (element) {
if (element.IsRated && element.TotalResult.Count !== 0) {
if (!(SideMenu.Datas.APerfs[element.UserScreenName])) {
console.log(element.UserScreenName)
}
else {
activePerf.push(SideMenu.Datas.APerfs[element.UserScreenName])
}
}
})
\$('#predictor-reload').button('reset')
switch(lastUpdated) {
case 0:
UpdatePredictorFromRank(\$("#predictor-input-rank").val())
break;
case 1:
UpdatePredictorFromPerf(\$("#predictor-input-perf").val())
break;
case 2:
UpdatePredictorFromRate(\$("#predictor-input-rate").val())
break;
}
}
function LoadAPerfs() {
\$('#predictor-reload').button('loading')
SideMenu.Datas.Update.APerfs()
.done(() => {
dicLength = Object.keys(SideMenu.Datas.APerfs).length;
\$("#predictor-alert").html(\`\`);
LoadStandings()
})
.fail(() => {
\$('#predictor-reload').button('reset')
\$("#predictor-input-rank").attr("disabled", "")
\$("#predictor-input-perf").attr("disabled", "")
\$("#predictor-input-rate").attr("disabled", "")
\$("#predictor-reload").attr("disabled", "")
\$("#predictor-current").attr("disabled", "")
\$("#predictor-tweet").attr("disabled", "")
\$("#predictor-alert").html("");
})
}
\$('#predictor-reload').click(function () {
LoadAPerfs()
})
function updatePredictorTweetBtn() {
var tweetStr =
\`Rated内順位: \${\$("#predictor-input-rank").val()}位%0A
パフォーマンス: \${\$("#predictor-input-perf").val()}%0A
レート: \${\$("#predictor-input-rate").val()}\`
\$('#predictor-tweet').attr("href", \`https://twitter.com/intent/tweet?text=\${tweetStr}\`)
}
var lastUpdated = 0;
\$('#predictor-input-rank').keyup(function(event) {
UpdatePredictorFromRank(\$("#predictor-input-rank").val())
});
\$('#predictor-input-perf').keyup(function(event) {
UpdatePredictorFromPerf(\$("#predictor-input-perf").val())
});
\$('#predictor-input-rate').keyup(function(event) {
UpdatePredictorFromRate(\$("#predictor-input-rate").val())
});
})();`;
var style =
``;
var dom =
`
`;
return `${dom}
`;
}
});
//Submit Status
SideMenu.Elements.SubmitStatus = (async () => {
await SideMenu.appendToSideMenu(/beta.atcoder.jp/,'Submit Status',getElem);
async function getElem() {
var js =
``;
var style =
``;
var dom =
``;
return `${dom}
`;
}
});
SideMenu.ViewOrder.forEach(async (elem) => {
await SideMenu.Elements[elem]();
});