// ==UserScript==
// @name ac-predictor
// @namespace http://ac-predictor.azurewebsites.net/
// @version 0.1.1
// @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/contests/*
// ==/UserScript==
embedData();
genSideMenu();
appendToSideMenu(getPredictorElem())
appendToSideMenu(getEstimatorElem())
function embedData() {
historyJsonURL = `https://beta.atcoder.jp/users/${userScreenName}/history/json`
standingsJsonURL = `https://beta.atcoder.jp/contests/${contestScreenName}/standings/json`
aperfsJsonURL = `https://ac-predictor.azurewebsites.net/api/aperfs/${contestScreenName}`
$.ajax({
url: historyJsonURL,
type: "GET",
dataType: "json"
}).done(function (history) {
historyObj = history
})
var maxDic = { "soundhound2018-summer-qual": 2400 };
maxPerf = (maxDic[contestScreenName] ? maxDic[contestScreenName] : (contestScreenName.substr(0, 3) === "abc" ? 1600 : (contestScreenName.substr(0, 3) === "arc" ? 3200 : 8192)))
function appendScriptToHead(script) {
var head = document.getElementsByTagName('head')[0];
var sideMenuScript = script;
var s = document.createElement("script");
s.innerHTML = script;
head.appendChild(s)
}
}
function appendToSideMenu(elem) {
$('#sidemenu').append(elem);
}
//サイドメニューを生成
function genSideMenu() {
var menuWidth = 350
var keyWidth = 50
var speed = 150
var sideMenuScript =
`<script>//参考:http://blog.8bit.co.jp/?p=12308
const activeClass = 'sidemenu-active'
var menuWrap = '#menu_wrap'
var sideMenu = '#sidemenu'
var sideMenuKey = '#sidemenu-key'
var menuWidth = ${menuWidth}
var keyWidth = ${keyWidth}
var speed = ${speed}
var windowHeight = $(window).height();
$(sideMenu).height(windowHeight);
//メニュー開閉
$(sideMenuKey).click(function () {
if ($(menuWrap).hasClass(activeClass)) {
//activeを削除
$(menuWrap).removeClass(activeClass);
//ボタンの文言を変更
$(sideMenuKey).removeClass('glyphicon-menu-right');
$(sideMenuKey).addClass('glyphicon-menu-left');
}
else {
//activeを付与
$(menuWrap).addClass(activeClass);
//ボタンの文言を変更
$(sideMenuKey).removeClass('glyphicon-menu-left');
$(sideMenuKey).addClass('glyphicon-menu-right');
};
});
//画面リサイズ時にheightを読み直し
var timer = false;
$(window).resize(function () {
if (timer !== false) {
clearTimeout(timer);
}
timer = setTimeout(function () {
windowHeight = $(window).height();
$(sideMenu).height(windowHeight);
}, 50);
});</script>`
var sideMenuStyle =
`<style>#menu_wrap{
display:block;
position:fixed;
top:0;
width:${keyWidth + menuWidth}px;
right:-${menuWidth}px;
transition: all ${speed}ms 0ms ease;
margin-top:50px;
}
#sidemenu{
background:#000;
opacity:0.85;
}
#sidemenu-key{
border-radius:5px 0px 0px 5px;
background:#000;
opacity:0.75;
color:#FFF;
padding:30px 0;
cursor:pointer;
margin-top:100px;
text-align: center;
}
#sidemenu{
display:inline-block;
width:${menuWidth}px;
float:right;
}
#sidemenu-key{
display:inline-block;
width:${keyWidth}px;
float:right;
}
.sidemenu-active{
transform: translateX(-${menuWidth}px);
}
.sidemenu-container{
padding: 10px 20px 10px 20px;
border-bottom: 1px solid #FFF;
}
.sidemenu-txt{
color: #DDD;
}
</style>`
var tweetScript =
`<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');</script>`
var ratingScript =
`<script src="https://koba-e964.github.io/atcoder-rating-estimator/atcoder_rating.js"></script>`
$('#main-div').append(`<div id="menu_wrap"><div id="sidemenu" class="container"></div><div id="sidemenu-key" class="glyphicon glyphicon-menu-left"></div>${tweetScript}${ratingScript}${sideMenuScript}${sideMenuStyle}</div>`)
}
function getPredictorElem() {
var predictorScript =
`<script>
if (!startTime.isBefore()) {
$("#estimator-input-rank").attr("disabled","")
$("#estimator-input-perf").attr("disabled","")
$("#estimator-input-rate").attr("disabled","")
$("#estimator-reload").attr("disabled","")
$("#estimator-current").attr("disabled","")
$("#estimator-tweet").attr("disabled","")
$("#predictor-alert").html("<h5 class='sidemenu-txt'>コンテストは始まっていません</h5>");
}
else {
LoadAPerfs()
if(!endTime.isBefore()) var loadTimer = setInterval(LoadAPerfs, 30000)
}
$('[data-toggle="tooltip"]').tooltip()
function UpdatePredictor(rank,perf,rate) {
$("#estimator-input-rank").val(round(rank))
$("#estimator-input-perf").val(round(perf))
$("#estimator-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 Math.min(maxPerf, positivize_rating(calc_rating(historyObj.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, 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;
}
}
$('#estimator-current').click(function () {
//自分の順位を確認
var myRank = 0;
var tiedList = []
var lastRank = 0;
var rank = 1;
var isContainedMe = false;
//全員回して自分が出てきたら順位更新フラグを立てる
standingsObj.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() {
$.ajax({
url: standingsJsonURL,
type: "GET",
dataType: "json"
}).done(function (standings) {
standingsObj = standings
CalcActivePerf()
})
}
function CalcActivePerf() {
activePerf = []
//Perf計算時に使うパフォ(Ratedオンリー)
standingsObj.StandingsData.forEach(function (element) {
if (element.IsRated && element.TotalResult.Count !== 0) {
if (!(aperfsObj[element.UserScreenName])) {
console.log(element.UserScreenName)
}
else {
activePerf.push(aperfsObj[element.UserScreenName])
}
}
})
$('#estimator-reload').button('reset')
switch(lastUpdated) {
case 0:
UpdatePredictorFromRank($("#estimator-input-rank").val())
break;
case 1:
UpdatePredictorFromPerf($("#estimator-input-perf").val())
break;
case 2:
UpdatePredictorFromRate($("#estimator-input-rate").val())
break;
}
}
function LoadAPerfs() {
$('#estimator-reload').button('loading')
$.ajax({
url: aperfsJsonURL,
type: "GET",
dataType: "json"
}).done(function (aperfs) {
aperfsObj = aperfs
dicLength = Object.keys(aperfsObj).length;
LoadStandings()
})
}
$('#estimator-reload').click(function () {
LoadAPerfs()
})
function updatePredictorTweetBtn() {
var tweetStr =
\`Rated内順位: \${$("#estimator-input-rank").val()}位%0A
パフォーマンス: \${$("#estimator-input-perf").val()}%0A
レート: \${$("#estimator-input-rate").val()}\`
$('#predictor-tweet').attr("href", \`https://twitter.com/intent/tweet?text=\${tweetStr}\`)
}
var lastUpdated = 0;
$('#estimator-input-rank').keyup(function(event) {
UpdatePredictorFromRank($("#estimator-input-rank").val())
});
$('#estimator-input-perf').keyup(function(event) {
UpdatePredictorFromPerf($("#estimator-input-perf").val())
});
$('#estimator-input-rate').keyup(function(event) {
UpdatePredictorFromRate($("#estimator-input-rate").val())
});</script>`
var dom =
`<div id="predictor" class="sidemenu-container">
<h3 class="sidemenu-txt">Rating Predictor</h3>
<div id="predictor-alert"></div>
<div id="predictor-data">
<div class="row">
<div class="input-group col-xs-offset-1 col-xs-10">
<span class="input-group-addon">順位<span class="glyphicon glyphicon-question-sign" aria-hidden="true" data-html="true" data-toggle="tooltip" title="" data-original-title="Rated内の順位です。複数人同順位の際は人数を加味します(5位が4人居たら6.5位として計算)"></span></span>
<input class="form-control" id="estimator-input-rank">
<span class="input-group-addon">位</span>
</div>
<div class="input-group col-xs-offset-1 col-xs-10">
<span class="input-group-addon">パフォーマンス</span>
<input class="form-control" id="estimator-input-perf">
</div>
<div class="input-group col-xs-offset-1 col-xs-10">
<span class="input-group-addon">レーティング</span>
<input class="form-control" id="estimator-input-rate">
</div>
</div>
</div>
<div class="btn-group">
<button class="btn btn-default" id="estimator-current">現在の順位</button>
<button type="button" class="btn btn-primary" id="estimator-reload" data-loading-text="更新中…">更新</button>
<a class="btn btn-default" rel="nofollow" onClick="window.open(encodeURI(decodeURI(this.href)),'twwindow','width=550, height=450, personalbar=0, toolbar=0, scrollbars=1'); return false;" id='predictor-tweet'>ツイート</a>
<!--<button class="btn btn-default" id="estimator-solved" disabled>現問題AC後</button>-->
</div>
<div id="predictor-reload">
<!--<h5 class="sidemenu-txt">更新設定</h5>-->
<div class="row">
<!--<div class="input-group col-xs-offset-1 col-xs-10">
<span class="input-group-addon" id="estimator-input-desc">自動更新</span>
<input type="number" class="form-control" id="estimator-input">
<span class="input-group-addon">秒</span>
</div>-->
</div>
</div>
${predictorScript}
</div>`;
return dom;
}
function getEstimatorElem() {
var estimatorScript =
`var estimator_state = 0;
$("#estimator-input").keyup(function () {
var input = $("#estimator-input").val();
if (!isFinite(input)) {
displayAlert("数字ではありません")
return;
}
var history = historyObj.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)
updateTweetBtn()
});
$("#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;
}
var val = $("#estimator-res").val();
$("#estimator-res").val($("#estimator-input").val())
$("#estimator-input").val(val)
updateTweetBtn()
})
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 dom =
`<div id="estimator" class="sidemenu-container">
<h3 class="sidemenu-txt">Rating Estimator</h3>
<div id="estimator-alert"></div>
<div class="row">
<div class="input-group">
<span class="input-group-addon" id="estimator-input-desc">目標レート</span>
<input type="number" class="form-control" id="estimator-input">
</div>
</div>
<div class="row">
<div class="input-group">
<span class="input-group-addon" id="estimator-res-desc">必要パフォーマンス</span>
<input class="form-control" id="estimator-res" disabled="disabled">
<span class="input-group-btn">
<button class="btn btn-default" id="estimator-toggle">入替</button>
</span>
</div>
</div>
<div class="row" style="margin: 10px 0px;">
<a class="btn btn-default col-xs-offset-8 col-xs-4" rel="nofollow" onClick="window.open(encodeURI(decodeURI(this.href)),'twwindow','width=550, height=450, personalbar=0, toolbar=0, scrollbars=1'); return false;" id='estimator-tweet'>ツイート</a>
</div>
<script>${estimatorScript}</script>
</div>`
return dom;
}