ac-predictor

コンテスト中にAtCoderのパフォーマンスを予測します

目前为 2018-07-01 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name ac-predictor
  3. // @namespace http://ac-predictor.azurewebsites.net/
  4. // @version 0.1.1
  5. // @description コンテスト中にAtCoderのパフォーマンスを予測します
  6. // @author keymoon
  7. // @license MIT
  8. // @homepage https://github.com/key-moon/ac-predictor
  9. // @supportURL https://github.com/key-moon/ac-predictor/issues
  10. // @match https://beta.atcoder.jp/contests/*
  11. // ==/UserScript==
  12. embedData();
  13. genSideMenu();
  14. appendToSideMenu(getPredictorElem())
  15. appendToSideMenu(getEstimatorElem())
  16.  
  17. function embedData() {
  18. historyJsonURL = `https://beta.atcoder.jp/users/${userScreenName}/history/json`
  19. standingsJsonURL = `https://beta.atcoder.jp/contests/${contestScreenName}/standings/json`
  20. aperfsJsonURL = `https://ac-predictor.azurewebsites.net/api/aperfs/${contestScreenName}`
  21. $.ajax({
  22. url: historyJsonURL,
  23. type: "GET",
  24. dataType: "json"
  25. }).done(function (history) {
  26. historyObj = history
  27. })
  28. var maxDic = { "soundhound2018-summer-qual": 2400 };
  29. maxPerf = (maxDic[contestScreenName] ? maxDic[contestScreenName] : (contestScreenName.substr(0, 3) === "abc" ? 1600 : (contestScreenName.substr(0, 3) === "arc" ? 3200 : 8192)))
  30. function appendScriptToHead(script) {
  31. var head = document.getElementsByTagName('head')[0];
  32. var sideMenuScript = script;
  33. var s = document.createElement("script");
  34. s.innerHTML = script;
  35. head.appendChild(s)
  36. }
  37. }
  38.  
  39.  
  40. function appendToSideMenu(elem) {
  41. $('#sidemenu').append(elem);
  42. }
  43.  
  44. //サイドメニューを生成
  45. function genSideMenu() {
  46. var menuWidth = 350
  47. var keyWidth = 50
  48. var speed = 150
  49. var sideMenuScript =
  50. `<script>//参考:http://blog.8bit.co.jp/?p=12308
  51. const activeClass = 'sidemenu-active'
  52. var menuWrap = '#menu_wrap'
  53. var sideMenu = '#sidemenu'
  54. var sideMenuKey = '#sidemenu-key'
  55. var menuWidth = ${menuWidth}
  56. var keyWidth = ${keyWidth}
  57. var speed = ${speed}
  58.  
  59. var windowHeight = $(window).height();
  60. $(sideMenu).height(windowHeight);
  61.  
  62. //メニュー開閉
  63. $(sideMenuKey).click(function () {
  64. if ($(menuWrap).hasClass(activeClass)) {
  65. //activeを削除
  66. $(menuWrap).removeClass(activeClass);
  67. //ボタンの文言を変更
  68. $(sideMenuKey).removeClass('glyphicon-menu-right');
  69. $(sideMenuKey).addClass('glyphicon-menu-left');
  70. }
  71. else {
  72. //activeを付与
  73. $(menuWrap).addClass(activeClass);
  74. //ボタンの文言を変更
  75. $(sideMenuKey).removeClass('glyphicon-menu-left');
  76. $(sideMenuKey).addClass('glyphicon-menu-right');
  77. };
  78. });
  79.  
  80. //画面リサイズ時にheightを読み直し
  81. var timer = false;
  82. $(window).resize(function () {
  83. if (timer !== false) {
  84. clearTimeout(timer);
  85. }
  86. timer = setTimeout(function () {
  87. windowHeight = $(window).height();
  88. $(sideMenu).height(windowHeight);
  89. }, 50);
  90. });</script>`
  91. var sideMenuStyle =
  92. `<style>#menu_wrap{
  93. display:block;
  94. position:fixed;
  95. top:0;
  96. width:${keyWidth + menuWidth}px;
  97. right:-${menuWidth}px;
  98. transition: all ${speed}ms 0ms ease;
  99. margin-top:50px;
  100. }
  101.  
  102. #sidemenu{
  103. background:#000;
  104. opacity:0.85;
  105. }
  106. #sidemenu-key{
  107. border-radius:5px 0px 0px 5px;
  108. background:#000;
  109. opacity:0.75;
  110. color:#FFF;
  111. padding:30px 0;
  112. cursor:pointer;
  113. margin-top:100px;
  114. text-align: center;
  115. }
  116.  
  117. #sidemenu{
  118. display:inline-block;
  119. width:${menuWidth}px;
  120. float:right;
  121. }
  122.  
  123. #sidemenu-key{
  124. display:inline-block;
  125. width:${keyWidth}px;
  126. float:right;
  127. }
  128.  
  129. .sidemenu-active{
  130. transform: translateX(-${menuWidth}px);
  131. }
  132.  
  133. .sidemenu-container{
  134. padding: 10px 20px 10px 20px;
  135. border-bottom: 1px solid #FFF;
  136. }
  137.  
  138. .sidemenu-txt{
  139. color: #DDD;
  140. }
  141. </style>`
  142. var tweetScript =
  143. `<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>`
  144. var ratingScript =
  145. `<script src="https://koba-e964.github.io/atcoder-rating-estimator/atcoder_rating.js"></script>`
  146. $('#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>`)
  147. }
  148.  
  149. function getPredictorElem() {
  150. var predictorScript =
  151. `<script>
  152. if (!startTime.isBefore()) {
  153. $("#estimator-input-rank").attr("disabled","")
  154. $("#estimator-input-perf").attr("disabled","")
  155. $("#estimator-input-rate").attr("disabled","")
  156. $("#estimator-reload").attr("disabled","")
  157. $("#estimator-current").attr("disabled","")
  158. $("#estimator-tweet").attr("disabled","")
  159. $("#predictor-alert").html("<h5 class='sidemenu-txt'>コンテストは始まっていません</h5>");
  160. }
  161. else {
  162. LoadAPerfs()
  163. if(!endTime.isBefore()) var loadTimer = setInterval(LoadAPerfs, 30000)
  164. }
  165.  
  166. $('[data-toggle="tooltip"]').tooltip()
  167. function UpdatePredictor(rank,perf,rate) {
  168. $("#estimator-input-rank").val(round(rank))
  169. $("#estimator-input-perf").val(round(perf))
  170. $("#estimator-input-rate").val(round(rate))
  171. updatePredictorTweetBtn()
  172. function round(val) {
  173. return Math.round(val * 100) / 100;
  174. }
  175. }
  176.  
  177. function UpdatePredictorFromRank(rank) {
  178. var perf = getPerf(rank)
  179. var rate = getRate(perf)
  180. lastUpdated = 0
  181. UpdatePredictor(rank,perf,rate)
  182. }
  183.  
  184. function UpdatePredictorFromPerf(perf) {
  185. var upper = 16384
  186. var lower = 0
  187. while(upper - lower > 0.125) {
  188. if (perf > getPerf(lower + (upper - lower) / 2)) upper -= (upper - lower) / 2
  189. else lower += (upper - lower) / 2
  190. }
  191. lastUpdated = 1
  192. var rank = lower + (upper - lower) / 2;
  193. var rate = getRate(perf)
  194. UpdatePredictor(rank,perf,rate)
  195. }
  196. function UpdatePredictorFromRate(rate) {
  197. var upper = 16384
  198. var lower = 0
  199. while(upper - lower > 0.125) {
  200. if (rate < getRate(lower + (upper - lower) / 2)) upper -= (upper - lower) / 2
  201. else lower += (upper - lower) / 2
  202. }
  203. lastUpdated = 2
  204. var perf = lower + (upper - lower) / 2;
  205. upper = 16384
  206. lower = 0
  207. while(upper - lower > 0.125) {
  208. if (perf > getPerf(lower + (upper - lower) / 2)) upper -= (upper - lower) / 2
  209. else lower += (upper - lower) / 2
  210. }
  211. var rank = lower + (upper - lower) / 2;
  212. UpdatePredictor(rank,perf,rate)
  213. }
  214.  
  215. function getRate(perf) {
  216. return Math.min(maxPerf, positivize_rating(calc_rating(historyObj.filter(x => x.IsRated).map(x => x.Performance).concat(perf).reverse())));
  217. }
  218.  
  219. function getPerf(rank) {
  220. var upper = 8192
  221. var lower = -8192
  222.  
  223. while (upper - lower > 0.5) {
  224. if (rank - 0.5 > calcPerf(lower + (upper - lower) / 2)) upper -= (upper - lower) / 2
  225. else lower += (upper - lower) / 2
  226. }
  227.  
  228. var innerPerf = Math.round(lower + (upper - lower) / 2)
  229.  
  230. return Math.min(innerPerf, maxPerf)
  231.  
  232. function calcPerf(X) {
  233. var res = 0;
  234. activePerf.forEach(function (APerf) {
  235. res += 1.0 / (1.0 + Math.pow(6.0, ((X - APerf) / 400.0)))
  236. })
  237. return res;
  238. }
  239. }
  240. $('#estimator-current').click(function () {
  241. //自分の順位を確認
  242. var myRank = 0;
  243.  
  244. var tiedList = []
  245. var lastRank = 0;
  246. var rank = 1;
  247. var isContainedMe = false;
  248. //全員回して自分が出てきたら順位更新フラグを立てる
  249. standingsObj.StandingsData.forEach(function (element) {
  250. if (!element.IsRated || element.TotalResult.Count === 0) return;
  251. if (lastRank !== element.Rank) {
  252. if (isContainedMe) {
  253. myRank = rank + (tiedList.length - 1) / 2;
  254. isContainedMe = false;
  255. }
  256. rank += tiedList.length;
  257. tiedList = []
  258. }
  259.  
  260. if (isContainedMe) {
  261. myRank = rank + (tiedList.length - 1) / 2;
  262. isContainedMe = false;
  263. }
  264.  
  265. if(userScreenName == element.UserScreenName) isContainedMe = true;
  266. tiedList.push(element)
  267. lastRank = element.Rank;
  268. })
  269. //存在しなかったら空欄
  270. if(myRank === 0) {
  271. UpdatePredictor("","","")
  272. }
  273. else {
  274. UpdatePredictorFromRank(myRank)
  275. }
  276. })
  277. function LoadStandings() {
  278. $.ajax({
  279. url: standingsJsonURL,
  280. type: "GET",
  281. dataType: "json"
  282. }).done(function (standings) {
  283. standingsObj = standings
  284. CalcActivePerf()
  285. })
  286. }
  287.  
  288. function CalcActivePerf() {
  289. activePerf = []
  290. //Perf計算時に使うパフォ(Ratedオンリー)
  291. standingsObj.StandingsData.forEach(function (element) {
  292. if (element.IsRated && element.TotalResult.Count !== 0) {
  293. if (!(aperfsObj[element.UserScreenName])) {
  294. console.log(element.UserScreenName)
  295. }
  296. else {
  297. activePerf.push(aperfsObj[element.UserScreenName])
  298. }
  299. }
  300. })
  301. $('#estimator-reload').button('reset')
  302. switch(lastUpdated) {
  303. case 0:
  304. UpdatePredictorFromRank($("#estimator-input-rank").val())
  305. break;
  306. case 1:
  307. UpdatePredictorFromPerf($("#estimator-input-perf").val())
  308. break;
  309. case 2:
  310. UpdatePredictorFromRate($("#estimator-input-rate").val())
  311. break;
  312. }
  313. }
  314.  
  315. function LoadAPerfs() {
  316. $('#estimator-reload').button('loading')
  317. $.ajax({
  318. url: aperfsJsonURL,
  319. type: "GET",
  320. dataType: "json"
  321. }).done(function (aperfs) {
  322. aperfsObj = aperfs
  323. dicLength = Object.keys(aperfsObj).length;
  324. LoadStandings()
  325. })
  326. }
  327.  
  328. $('#estimator-reload').click(function () {
  329. LoadAPerfs()
  330. })
  331. function updatePredictorTweetBtn() {
  332. var tweetStr =
  333. \`Rated内順位: \${$("#estimator-input-rank").val()}位%0A
  334. パフォーマンス: \${$("#estimator-input-perf").val()}%0A
  335. レート: \${$("#estimator-input-rate").val()}\`
  336. $('#predictor-tweet').attr("href", \`https://twitter.com/intent/tweet?text=\${tweetStr}\`)
  337. }
  338. var lastUpdated = 0;
  339. $('#estimator-input-rank').keyup(function(event) {
  340. UpdatePredictorFromRank($("#estimator-input-rank").val())
  341. });
  342. $('#estimator-input-perf').keyup(function(event) {
  343. UpdatePredictorFromPerf($("#estimator-input-perf").val())
  344. });
  345. $('#estimator-input-rate').keyup(function(event) {
  346. UpdatePredictorFromRate($("#estimator-input-rate").val())
  347. });</script>`
  348.  
  349. var dom =
  350. `<div id="predictor" class="sidemenu-container">
  351. <h3 class="sidemenu-txt">Rating Predictor</h3>
  352. <div id="predictor-alert"></div>
  353. <div id="predictor-data">
  354. <div class="row">
  355. <div class="input-group col-xs-offset-1 col-xs-10">
  356. <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>
  357. <input class="form-control" id="estimator-input-rank">
  358. <span class="input-group-addon">位</span>
  359. </div>
  360. <div class="input-group col-xs-offset-1 col-xs-10">
  361. <span class="input-group-addon">パフォーマンス</span>
  362. <input class="form-control" id="estimator-input-perf">
  363. </div>
  364.  
  365. <div class="input-group col-xs-offset-1 col-xs-10">
  366. <span class="input-group-addon">レーティング</span>
  367. <input class="form-control" id="estimator-input-rate">
  368. </div>
  369. </div>
  370. </div>
  371. <div class="btn-group">
  372. <button class="btn btn-default" id="estimator-current">現在の順位</button>
  373. <button type="button" class="btn btn-primary" id="estimator-reload" data-loading-text="更新中…">更新</button>
  374. <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>
  375. <!--<button class="btn btn-default" id="estimator-solved" disabled>現問題AC後</button>-->
  376. </div>
  377. <div id="predictor-reload">
  378. <!--<h5 class="sidemenu-txt">更新設定</h5>-->
  379. <div class="row">
  380. <!--<div class="input-group col-xs-offset-1 col-xs-10">
  381. <span class="input-group-addon" id="estimator-input-desc">自動更新</span>
  382. <input type="number" class="form-control" id="estimator-input">
  383. <span class="input-group-addon">秒</span>
  384. </div>-->
  385. </div>
  386. </div>
  387. ${predictorScript}
  388. </div>`;
  389. return dom;
  390. }
  391.  
  392. function getEstimatorElem() {
  393. var estimatorScript =
  394. `var estimator_state = 0;
  395. $("#estimator-input").keyup(function () {
  396. var input = $("#estimator-input").val();
  397. if (!isFinite(input)) {
  398. displayAlert("数字ではありません")
  399. return;
  400. }
  401. var history = historyObj.filter(x => x.IsRated)
  402. history.sort(function (a, b) {
  403. if (a.EndTime < b.EndTime) return 1;
  404. if (a.EndTime > b.EndTime) return -1;
  405. return 0;
  406. })
  407. history = history.map(x => x.InnerPerformance)
  408. var input = parseInt(input, 10)
  409. var res = -1;
  410. if (estimator_state === 0) {
  411. // binary search
  412. var goal_rating = unpositivize_rating(input)
  413. var lo = -10000.0;
  414. var hi = 10000.0;
  415. for (var i = 0; i < 100; ++i) {
  416. var mid = (hi + lo) / 2;
  417. var r = calc_rating([mid].concat(history));
  418. if (r >= goal_rating) {
  419. hi = mid;
  420. } else {
  421. lo = mid;
  422. }
  423. }
  424. res = (hi + lo) / 2;
  425. }
  426. else {
  427. res = calc_rating([input].concat(history));
  428. }
  429. res = Math.round(res * 100) / 100
  430. $("#estimator-res").val(res)
  431. updateTweetBtn()
  432. });
  433.  
  434. $("#estimator-toggle").click(function () {
  435. if (estimator_state === 0) {
  436. $("#estimator-input-desc").text("パフォーマンス")
  437. $("#estimator-res-desc").text("到達レーティング")
  438. estimator_state = 1;
  439. }
  440. else {
  441. $("#estimator-input-desc").text("目標レーティング")
  442. $("#estimator-res-desc").text("必要パフォーマンス")
  443. estimator_state = 0;
  444. }
  445. var val = $("#estimator-res").val();
  446. $("#estimator-res").val($("#estimator-input").val())
  447. $("#estimator-input").val(val)
  448. updateTweetBtn()
  449. })
  450.  
  451. function updateTweetBtn() {
  452. var tweetStr =
  453. \`AtCoderのハンドルネーム: \${userScreenName}%0A
  454. \${estimator_state == 0 ? "目標レーティング" : "パフォーマンス"}: \${$("#estimator-input").val()}%0A
  455. \${estimator_state == 0 ? "必要パフォーマンス" : "到達レーティング"}: \${$("#estimator-res").val()}\`
  456. $('#estimator-tweet').attr("href", \`https://twitter.com/intent/tweet?text=\${tweetStr}\`)
  457. }
  458.  
  459. function displayAlert (message) {
  460. var alertDiv = document.createElement('div')
  461. alertDiv.setAttribute("role", "alert")
  462. alertDiv.setAttribute("class", "alert alert-warning alert-dismissible")
  463. var closeButton = document.createElement('button')
  464. closeButton.setAttribute("type", "button")
  465. closeButton.setAttribute("class", "close")
  466. closeButton.setAttribute("data-dismiss", "alert")
  467. closeButton.setAttribute("aria-label", "閉じる")
  468. var closeSpan = document.createElement('span')
  469. closeSpan.setAttribute("aria-hidden", "true")
  470. closeSpan.textContent = "×"
  471. closeButton.appendChild(closeSpan)
  472. var messageContent = document.createTextNode(message)
  473. alertDiv.appendChild(closeButton)
  474. alertDiv.appendChild(messageContent)
  475. $("#estimator-alert").append(alertDiv)
  476. }`
  477. var dom =
  478. `<div id="estimator" class="sidemenu-container">
  479. <h3 class="sidemenu-txt">Rating Estimator</h3>
  480. <div id="estimator-alert"></div>
  481. <div class="row">
  482. <div class="input-group">
  483. <span class="input-group-addon" id="estimator-input-desc">目標レート</span>
  484. <input type="number" class="form-control" id="estimator-input">
  485. </div>
  486. </div>
  487. <div class="row">
  488. <div class="input-group">
  489. <span class="input-group-addon" id="estimator-res-desc">必要パフォーマンス</span>
  490. <input class="form-control" id="estimator-res" disabled="disabled">
  491. <span class="input-group-btn">
  492. <button class="btn btn-default" id="estimator-toggle">入替</button>
  493. </span>
  494. </div>
  495. </div>
  496. <div class="row" style="margin: 10px 0px;">
  497. <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>
  498. </div>
  499. <script>${estimatorScript}</script>
  500. </div>`
  501. return dom;
  502. }