// ==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(\`
最終更新 : \${moment().format('HH:mm:ss')}
\`); 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](); });