- // ==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;
- }