ac-predictor

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

当前为 2018-08-20 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name ac-predictor
  3. // @namespace http://ac-predictor.azurewebsites.net/
  4. // @version 1.0.1
  5. // @description コンテスト中にAtCoderのパフォーマンスを予測します
  6. // @author keymoon
  7. // @license MIT
  8. // @supportURL https://github.com/key-moon/ac-predictor.user.js/issues
  9. // @match https://beta.atcoder.jp/*
  10. // @exclude https://beta.atcoder.jp/*/json
  11. // ==/UserScript==
  12.  
  13. //NameSpace
  14. SideMenu = {};
  15.  
  16. SideMenu.Version = 1.0;
  17.  
  18. //共有データセット(HistoryとかStandingsとか)
  19. SideMenu.Datas = {};
  20. //共有データセットそれぞれをUpdateする関数を入れておく
  21. SideMenu.Datas.Update = {}
  22.  
  23. //Datas.Update内に関数を追加
  24. //History
  25. SideMenu.Datas.History = null;
  26. SideMenu.Datas.Update.History = (() => {
  27. var d = $.Deferred();
  28. try {
  29. $.ajax({
  30. url: `https://beta.atcoder.jp/users/${userScreenName}/history/json`,
  31. type: "GET",
  32. dataType: "json"
  33. }).done(history => {
  34. SideMenu.Datas.History = history;
  35. d.resolve();
  36. }).fail(() => { d.reject(); });
  37. }
  38. catch (e) {
  39. d.reject(e);
  40. }
  41. return d.promise();
  42. });
  43.  
  44. //Standings
  45. SideMenu.Datas.Standings = null;
  46. SideMenu.Datas.Update.Standings = (() => {
  47. var d = $.Deferred();
  48. try {
  49. $.ajax({
  50. url: `https://beta.atcoder.jp/contests/${contestScreenName}/standings/json`,
  51. type: "GET",
  52. dataType: "json"
  53. }).done(standings => {
  54. SideMenu.Datas.Standings = standings;
  55. d.resolve();
  56. }).fail(() => { d.reject(); });
  57. }
  58. catch (e) {
  59. d.reject();
  60. }
  61. return d.promise();
  62. });
  63.  
  64. //APerfs
  65. SideMenu.Datas.APerfs = null;
  66. SideMenu.Datas.Update.APerfs = (() => {
  67. var d = $.Deferred();
  68. try {
  69. $.ajax({
  70. url: `https://ac-predictor.azurewebsites.net/api/aperfs/${contestScreenName}`,
  71. type: "GET",
  72. dataType: "json"
  73. }).done(aperfs => {
  74. SideMenu.Datas.APerfs = aperfs
  75. d.resolve();
  76. }).fail(() => { d.reject(); });
  77. }
  78. catch (e) {
  79. d.reject();
  80. }
  81. return d.promise();
  82. });
  83.  
  84. //ライブラリを追加するやつ
  85. SideMenu.appendLibrary = function (source) {
  86. var defferd = $.Deferred();
  87. $.ajax({
  88. url: source
  89. }).done(((src) => {
  90. $('head').append(`<script>${src}</script>`);
  91. defferd.resolve();
  92. })(() => {
  93. defferd.fail();
  94. }));
  95. return defferd.promise();
  96. };
  97.  
  98.  
  99.  
  100. //通知関連
  101. SideMenu.Notifications = {};
  102. SideMenu.Notifications.CanSend = false;
  103. (async () => {
  104. if (Notification.permission === 'denied') return;
  105. if (Notification.permission === 'default') {
  106. await (async () => {
  107. var defferd = $.Deferred();
  108. Notification.requestPermission((permission) => {
  109. SideMenu.Notifications.CanSend = permission === 'granted';
  110. defferd.resolve();
  111. })();
  112. return defferd.promise();
  113. });
  114. if (!Notification.permission) return;
  115. }
  116. })();
  117.  
  118. SideMenu.appendLibrary("https://koba-e964.github.io/atcoder-rating-estimator/atcoder_rating.js");
  119.  
  120. //サイドメニュー追加(将来仕様変更が起きる可能性大)
  121. SideMenu.appendToSideMenu = async function (match, title, elemFunc) {
  122. var defferd = $.Deferred();
  123. try {
  124. if (!match.test(location.href)) return;
  125. //アコーディオンメニュー
  126. var dom =
  127. `<div class="menu-wrapper">
  128. <div class="menu-header">
  129. <h4 class="sidemenu-txt">${title}<span class="glyphicon glyphicon-menu-up" style="float: right"></span></h4>
  130. </div>
  131. <div class="menu-box"><div class="menu-content">${await elemFunc()}</div></div>
  132. </div>`
  133. $('#sidemenu').append(dom);
  134. var contents = $('.menu-content');
  135. var contentElem = contents[contents.length - 1];
  136. $(contentElem).parents('.menu-box').css('height', contentElem.scrollHeight)
  137. defferd.resolve();
  138. }
  139. catch (e) {
  140. console.error(e);
  141. defferd.reject();
  142. }
  143. return defferd.promise();
  144. };
  145.  
  146.  
  147. //サイドメニューを生成
  148. (() => {
  149. var menuWidth = 350
  150. var keyWidth = 50
  151. var speed = 150
  152. var sideMenuScript =
  153. `<script>//参考:http://blog.8bit.co.jp/?p=12308
  154. (() => {
  155. const activeClass = 'sidemenu-active'
  156. var menuWrap = '#menu_wrap'
  157. var sideMenu = '#sidemenu'
  158. var sideMenuKey = '#sidemenu-key'
  159. var menuWidth = ${menuWidth}
  160. var keyWidth = ${keyWidth}
  161. var speed = ${speed}
  162.  
  163. var windowHeight = $(window).height();
  164. $(sideMenu).height(windowHeight);
  165.  
  166. //メニュー開閉
  167. $(sideMenuKey).click(function () {
  168. //ボタンの文言を変更する部分をCSSTransitionでやらせたい
  169. $(sideMenuKey).toggleClass('glyphicon-menu-left');
  170. $(sideMenuKey).toggleClass('glyphicon-menu-right');
  171. $(menuWrap).toggleClass(activeClass);
  172. });
  173.  
  174. //画面リサイズ時にheightを読み直し
  175. var timer = false;
  176. $(window).resize(function () {
  177. if (timer !== false) {
  178. clearTimeout(timer);
  179. }
  180. timer = setTimeout(function () {
  181. windowHeight = $(window).height();
  182. $(sideMenu).height(windowHeight);
  183. }, 50);
  184. });
  185.  
  186. //アコーディオンメニューのなんか
  187. $('#sidemenu').on('click','.menu-header',(event) => {
  188. //$(event.target).parents('.menu-wrapper').find('.menu-content').toggleClass('menu-content-collapse')
  189. $(event.target).parents('.menu-wrapper').find('.menu-box').toggleClass('menu-box-collapse')
  190. $(event.target).find('.glyphicon').toggleClass('glyphicon-menu-down')
  191. $(event.target).find('.glyphicon').toggleClass('glyphicon-menu-up')
  192. })
  193. /*$('.menu-content').exResize((event) => {
  194. $(event.target).parents('.menu-box').css('height',event.target.clientHeight)
  195. });*/
  196.  
  197. })();
  198. </script>`
  199. var sideMenuStyle =
  200. `<style>#menu_wrap{
  201. display:block;
  202. position:fixed;
  203. top:0;
  204. width:${keyWidth + menuWidth}px;
  205. right:-${menuWidth}px;
  206. transition: all ${speed}ms 0ms ease;
  207. margin-top:50px;
  208. }
  209.  
  210. #sidemenu{
  211. background:#000;
  212. opacity:0.85;
  213. }
  214. #sidemenu-key{
  215. border-radius:5px 0px 0px 5px;
  216. background:#000;
  217. opacity:0.75;
  218. color:#FFF;
  219. padding:30px 0;
  220. cursor:pointer;
  221. margin-top:100px;
  222. text-align: center;
  223. }
  224.  
  225. #sidemenu{
  226. display:inline-block;
  227. width:${menuWidth}px;
  228. float:right;
  229. }
  230.  
  231. #sidemenu-key{
  232. display:inline-block;
  233. width:${keyWidth}px;
  234. float:right;
  235. }
  236.  
  237. .sidemenu-active{
  238. transform: translateX(-${menuWidth}px);
  239. }
  240.  
  241. .sidemenu-txt{
  242. color: #DDD;
  243. }
  244. /*アコーディオンメニュー*/
  245. .menu-wrapper{
  246. border-bottom: 1px solid #FFF;
  247. }
  248. .menu-header h4{
  249. user-select: none;
  250. }
  251. .menu-header{
  252. margin:10px 20px 10px 20px;
  253. }
  254.  
  255. .menu-box{
  256. overflow: hidden;
  257. transition: all 300ms 0s ease;
  258. }
  259. .menu-box-collapse{
  260. height:0px !important;/**/
  261. }
  262.  
  263. .menu-content{
  264. padding: 10px 20px 10px 20px;
  265. transition: all 300ms 0s ease;
  266. }
  267. .menu-box-collapse .menu-content{
  268. transform: translateY(-100%);
  269. }
  270. </style>`
  271. var ratingScript =
  272. `<script>from: https://koba-e964.github.io/atcoder-rating-estimator/atcoder_rating.js
  273. function bigf(n) {
  274. var num = 1.0;
  275. var den = 1.0;
  276. for (var i = 0; i < n; ++i) {
  277. num *= 0.81;
  278. den *= 0.9;
  279. }
  280. num = (1 - num) * 0.81 / 0.19;
  281. den = (1 - den) * 0.9 / 0.1;
  282. return Math.sqrt(num) / den;
  283. }
  284.  
  285. function f(n) {
  286. var finf = bigf(400);
  287. return (bigf(n) - finf) / (bigf(1) - finf) * 1200.0;
  288. }
  289.  
  290. // Returns unpositivized ratings.
  291. function calc_rating(arr) {
  292. var n = arr.length;
  293. var num = 0.0;
  294. var den = 0.0;
  295. for (var i = n - 1; i >= 0; --i) {
  296. num *= 0.9;
  297. num += 0.9 * Math.pow(2, arr[i] / 800.0);
  298. den *= 0.9;
  299. den += 0.9;
  300. }
  301. var rating = Math.log2(num / den) * 800.0;
  302. rating -= f(n);
  303. return rating;
  304. }
  305.  
  306. // Takes and returns unpositivized ratings.
  307. function calc_rating_from_last(last, perf, n) {
  308. last += f(n);
  309. var wei = 9 - 9 * 0.9 ** n;
  310. var num = wei * (2 ** (last / 800.0)) + 2 ** (perf / 800.0) ;
  311. var den = 1 + wei;
  312. var rating = Math.log2(num / den) * 800.0;
  313. rating -= f(n + 1);
  314. return rating;
  315. }
  316.  
  317. // (-inf, inf) -> (0, inf)
  318. function positivize_rating(r) {
  319. if (r >= 400.0) {
  320. return r;
  321. }
  322. return 400.0 * Math.exp((r - 400.0) / 400.0);
  323. }
  324.  
  325. // (0, inf) -> (-inf, inf)
  326. function unpositivize_rating(r) {
  327. if (r >= 400.0) {
  328. return r;
  329. }
  330. return 400.0 + 400.0 * Math.log(r / 400.0);
  331. }</script>`;
  332. $('#main-div').append(`<div id="menu_wrap"><div id="sidemenu" class="container"></div><div id="sidemenu-key" class="glyphicon glyphicon-menu-left"></div>${ratingScript}${sideMenuScript}${sideMenuStyle}</div>`);
  333. })();
  334.  
  335.  
  336. //IndexedDB DB
  337. SideMenu.DataBase = {};
  338. SideMenu.DataBase.Name = "PredictorDB";
  339. SideMenu.DataBase.StoreNames = ["APerfs", "Standings"];
  340. indexedDB.open(SideMenu.DataBase.Name, SideMenu.Version).onupgradeneeded = (event) => {
  341. var db = event.target.result;
  342. SideMenu.DataBase.StoreNames.forEach(store => {
  343. db.createObjectStore(store, { keyPath: "id" });
  344. });
  345. };
  346. SideMenu.DataBase.SetData = (store, key, value) => {
  347. var defferd = $.Deferred();
  348. try {
  349. indexedDB.open(SideMenu.DataBase.Name).onsuccess = (e) => {
  350. var db = e.target.result;
  351. var trans = db.transaction(store, 'readwrite');
  352. var objStore = trans.objectStore(store);
  353. var data = { id: key, data: value };
  354. var putReq = objStore.put(data);
  355. putReq.onsuccess = function () {
  356. defferd.resolve();
  357. }
  358. }
  359. }
  360. catch (e) {
  361. defferd.reject(e);
  362. }
  363. return defferd.promise();
  364. };
  365. SideMenu.DataBase.GetData = (store, key) => {
  366. var defferd = $.Deferred();
  367. try {
  368. indexedDB.open(SideMenu.DataBase.Name).onsuccess = (e) => {
  369. var db = e.target.result;
  370. var trans = db.transaction(store, 'readwrite');
  371. var objStore = trans.objectStore(store);
  372. objStore.get(key).onsuccess = function (event) {
  373. var result = event.target.result;
  374. if (!result) defferd.reject("key was not found");
  375. else defferd.resolve(result.data);
  376. };
  377. }
  378. }
  379. catch (e) {
  380. defferd.reject(e);
  381. }
  382. return defferd.promise();
  383. };
  384.  
  385.  
  386. //サイドメニュー要素の入れ物
  387. SideMenu.Elements = {};
  388. SideMenu.ViewOrder = ["Predictor", "Estimator"];
  389.  
  390. SideMenu.Colors = ["unrated", "gray", "brown", "green", "cyan", "blue", "yellow", "orange", "red"];
  391. SideMenu.GetColor = (rating) => {
  392. var colorIndex = 0
  393. if (rating > 0) {
  394. colorIndex = Math.min(Math.floor(rating / 400) + 1, 8)
  395. }
  396. return SideMenu.Colors[colorIndex]
  397. };
  398.  
  399. //Estimator
  400. SideMenu.Elements.Estimator = (async () => {
  401. await SideMenu.appendToSideMenu(/beta.atcoder.jp/,'Estimator',getElem);
  402. async function getElem() {
  403. $("#estimator-input").val(localStorage.getItem("sidemenu_estimator_value"));
  404. if (!SideMenu.Datas.History) await SideMenu.Datas.Update.History();
  405. var js =
  406. `(() => {
  407. var estimator_state = localStorage.getItem("sidemenu_estimator_state");
  408. \$("#estimator-input").val(localStorage.getItem("sidemenu_estimator_value"));
  409. updateInputs();
  410. \$("#estimator-input").keyup(updateInputs);
  411. \$("#estimator-toggle").click(function () {
  412. \$("#estimator-input").val(\$("#estimator-res").val());
  413. estimator_state = (estimator_state + 1) % 2;
  414. updateInputs();
  415. })
  416. function updateInputs() {
  417. var input = \$("#estimator-input").val();
  418. if (!isFinite(input)) {
  419. displayAlert("数字ではありません")
  420. return;
  421. }
  422. var history = SideMenu.Datas.History.filter(x => x.IsRated)
  423. history.sort(function (a, b) {
  424. if (a.EndTime < b.EndTime) return 1;
  425. if (a.EndTime > b.EndTime) return -1;
  426. return 0;
  427. })
  428. history = history.map(x => x.InnerPerformance)
  429. var input = parseInt(input)
  430. var res = -1;
  431. if (estimator_state === 0) {
  432. \/\/ binary search
  433. var goal_rating = unpositivize_rating(input)
  434. var lo = -10000.0;
  435. var hi = 10000.0;
  436. for (var i = 0; i < 100; ++i) {
  437. var mid = (hi + lo) \/ 2;
  438. var r = calc_rating([mid].concat(history));
  439. if (r >= goal_rating) {
  440. hi = mid;
  441. } else {
  442. lo = mid;
  443. }
  444. }
  445. res = (hi + lo) \/ 2;
  446. \$("#estimator-input-desc").text("目標レーティング");
  447. \$("#estimator-res-desc").text("必要パフォーマンス");
  448. }
  449. else {
  450. res = calc_rating([input].concat(history));
  451. \$("#estimator-input-desc").text("パフォーマンス");
  452. \$("#estimator-res-desc").text("到達レーティング");
  453. }
  454. res = Math.round(res * 100) \/ 100
  455. if (!isNaN(res)) \$("#estimator-res").val(res);
  456. updateLocalStorage();
  457. updateTweetBtn();
  458. }
  459. function updateLocalStorage() {
  460. localStorage.setItem("sidemenu_estimator_state", estimator_state);
  461. localStorage.setItem("sidemenu_estimator_value", \$("#estimator-input").val());
  462. }
  463. function updateTweetBtn() {
  464. var tweetStr =
  465. \`AtCoderのハンドルネーム: \${userScreenName}%0A
  466. \${estimator_state == 0 ? "目標レーティング" : "パフォーマンス"}: \${\$("#estimator-input").val()}%0A
  467. \${estimator_state == 0 ? "必要パフォーマンス" : "到達レーティング"}: \${\$("#estimator-res").val()}\`
  468. \$('#estimator-tweet').attr("href", \`https:\/\/twitter.com\/intent\/tweet?text=\${tweetStr}\`)
  469. }
  470. function displayAlert(message) {
  471. var alertDiv = document.createElement('div')
  472. alertDiv.setAttribute("role", "alert")
  473. alertDiv.setAttribute("class", "alert alert-warning alert-dismissible")
  474. var closeButton = document.createElement('button')
  475. closeButton.setAttribute("type", "button")
  476. closeButton.setAttribute("class", "close")
  477. closeButton.setAttribute("data-dismiss", "alert")
  478. closeButton.setAttribute("aria-label", "閉じる")
  479. var closeSpan = document.createElement('span')
  480. closeSpan.setAttribute("aria-hidden", "true")
  481. closeSpan.textContent = "×"
  482. closeButton.appendChild(closeSpan)
  483. var messageContent = document.createTextNode(message)
  484. alertDiv.appendChild(closeButton)
  485. alertDiv.appendChild(messageContent)
  486. \$("#estimator-alert").append(alertDiv)
  487. }
  488. })();`;
  489. var style =
  490. ``;
  491. var dom =
  492. `<div id="estimator-alert"><\/div>
  493. <div class="row">
  494. <div class="input-group">
  495. <span class="input-group-addon" id="estimator-input-desc">目標レーティング<\/span>
  496. <input type="number" class="form-control" id="estimator-input">
  497. <\/div>
  498. <\/div>
  499. <div class="row">
  500. <div class="input-group">
  501. <span class="input-group-addon" id="estimator-res-desc">必要パフォーマンス<\/span>
  502. <input class="form-control" id="estimator-res" disabled="disabled">
  503. <span class="input-group-btn">
  504. <button class="btn btn-default" id="estimator-toggle">入替<\/button>
  505. <\/span>
  506. <\/div>
  507. <\/div>
  508. <div class="row" style="margin: 10px 0px;">
  509. <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>
  510. <\/div>`;
  511. return `${dom}
  512. <script>${js}</script>
  513. <style>${style}</style>`;
  514. }
  515. });
  516.  
  517. //Predictor
  518. SideMenu.Elements.Predictor = (async () => {
  519. await SideMenu.appendToSideMenu(/beta.atcoder.jp\/contests\/.*/,'Predictor',getElem);
  520. async function getElem() {
  521. //NameSpace
  522. SideMenu.Predictor = {};
  523. var maxDic =
  524. [
  525. [/^abc\d{3}$/, 1600],
  526. [/^arc\d{3}$/, 3200],
  527. [/^agc\d{3}$/, 8192],
  528. [/^apc\d{3}$/, 8192],
  529. [/^cf\d{2}-final-open$/, 8192],
  530. [/^soundhound2018-summer-qual$/, 2400],
  531. [/.*/, -1]
  532. ];
  533. SideMenu.Predictor.maxPerf = maxDic.filter(x => x[0].exec(contestScreenName))[0][1];
  534. if (!SideMenu.Datas.History) await SideMenu.Datas.Update.History().done(() => { isDone = true });
  535. var js =
  536. `(() => {
  537. \/\/各参加者の結果
  538. var eachParticipationResults = {};
  539. var isAlreadyAppendRowToStandings = false;
  540. const specialContest = ['practice', 'APG4b', 'abs'];
  541. const predictorElements = ['predictor-input-rank', 'predictor-input-perf', 'predictor-input-rate', 'predictor-current', 'predictor-reload', 'predictor-tweet'];
  542. const firstContestDate = moment("2016-07-16 21:00");
  543. const Interval = 30000;
  544. const ratedLimit = contestScreenName === "SoundHound Inc. Programming Contest 2018 -Masters Tournament-"
  545. ? 2000 : (\/abc\\d{3}\/.test(contestScreenName) ? 1200 : (\/arc\\d{3}\/.test(contestScreenName) ? 2800 : Infinity));
  546. const defaultAPerf = \/abc\\d{3}\/.test(contestScreenName) ? 800 : 1600;
  547. const isStandingsPage = \/standings(\\\/.*)?\$\/.test(document.location);
  548. \$('[data-toggle="tooltip"]').tooltip();
  549. \$('#predictor-reload').click(function () {
  550. UpdatePredictorsData();
  551. });
  552. \$('#predictor-current').click(function () {
  553. \/\/自分の順位を確認
  554. var myRank = 0;
  555. var ratedCount = 0;
  556. var lastRank = 0;
  557. var rank = 1;
  558. var isContainedMe = false;
  559. \/\/全員回して自分が出てきたら順位更新フラグを立てる
  560. SideMenu.Datas.Standings.StandingsData.forEach(function (element) {
  561. if (lastRank !== element.Rank) {
  562. if (isContainedMe) {
  563. myRank = rank + Math.max(0, ratedCount - 1) \/ 2;
  564. isContainedMe = false;
  565. }
  566. rank += ratedCount;
  567. ratedCount = 0;
  568. }
  569. if (userScreenName === element.UserScreenName) isContainedMe = true;
  570. if (element.IsRated && element.TotalResult.Count !== 0) ratedCount++;
  571. lastRank = element.Rank;
  572. })
  573. if (isContainedMe) {
  574. myRank = rank + ratedCount \/ 2;
  575. }
  576. if (myRank === 0) return;
  577. \$('#predictor-input-rank').val(myRank);
  578. lastUpdated = 0;
  579. drawPredictor();
  580. });
  581. \$('#predictor-input-rank').keyup(function (event) {
  582. lastUpdated = 0;
  583. drawPredictor();
  584. });
  585. \$('#predictor-input-perf').keyup(function (event) {
  586. lastUpdated = 1;
  587. drawPredictor();
  588. });
  589. \$('#predictor-input-rate').keyup(function (event) {
  590. lastUpdated = 2;
  591. drawPredictor();
  592. });
  593. var lastUpdated = 0;
  594. if (!startTime.isBefore()) {
  595. disabled();
  596. AddAlert('コンテストは始まっていません');
  597. return;
  598. }
  599. if (moment(startTime) < firstContestDate) {
  600. disabled();
  601. AddAlert('現行レートシステム以前のコンテストです');
  602. return;
  603. }
  604. if (specialContest.indexOf(contestScreenName) >= 0) {
  605. disabled();
  606. AddAlert('コンテストではありません');
  607. return;
  608. }
  609. if (!endTime.isBefore()) {
  610. SetUpdateInterval();
  611. return;
  612. }
  613. \$.when(
  614. SideMenu.DataBase.GetData("APerfs", contestScreenName),
  615. SideMenu.DataBase.GetData("Standings", contestScreenName)
  616. ).done((aperfs, standings) => {
  617. SideMenu.Datas.APerfs = aperfs;
  618. SideMenu.Datas.Standings = standings;
  619. CalcActivePerf();
  620. drawPredictor();
  621. enabled();
  622. AddAlert('ローカルストレージから取得されました。');
  623. if (isStandingsPage) {
  624. updateResultsData();
  625. addPerfToStandings();
  626. }
  627. }).fail(() => {
  628. UpdatePredictorsData();
  629. })
  630. \/\/再描画をの期間を再更新する
  631. function SetUpdateInterval() {
  632. UpdatePredictorsData();
  633. if (!endTime.isBefore()) setTimeout(SetUpdateInterval, Interval);
  634. }
  635. \/\/自分のレートをパフォから求める
  636. function getRate(perf) {
  637. return positivize_rating(calc_rating(SideMenu.Datas.History.filter(x => x.IsRated).map(x => x.Performance).concat(perf).reverse()));
  638. }
  639. \/\/パフォを順位から求める()
  640. function getPerf(rank) {
  641. var upper = 8192
  642. var lower = -8192
  643. while (upper - lower > 0.5) {
  644. if (rank - 0.5 > calcRankVal(lower + (upper - lower) \/ 2)) upper -= (upper - lower) \/ 2
  645. else lower += (upper - lower) \/ 2
  646. }
  647. var innerPerf = Math.round(lower + (upper - lower) \/ 2)
  648. return Math.min(innerPerf, SideMenu.Predictor.maxPerf);
  649. }
  650. \/\/パフォを求める際に出てくるパフォごとの順位を求める
  651. function calcRankVal(X) {
  652. var res = 0;
  653. activePerf.forEach(function (APerf) {
  654. res += 1.0 \/ (1.0 + Math.pow(6.0, ((X - APerf) \/ 400.0)))
  655. })
  656. return res;
  657. }
  658. \/\/データを更新して描画する
  659. function UpdatePredictorsData() {
  660. if (!startTime.isBefore()) {
  661. disabled();
  662. AddAlert('コンテストは始まっていません');
  663. return;
  664. }
  665. if (moment(startTime) < firstContestDate) {
  666. disabled();
  667. AddAlert('現行レートシステム以前のコンテストです');
  668. return;
  669. }
  670. if (specialContest.indexOf(contestScreenName) >= 0) {
  671. disabled();
  672. AddAlert('コンテストではありません');
  673. return;
  674. }
  675. \$('#predictor-reload').button('loading');
  676. AddAlert('順位表読み込み中…');
  677. SideMenu.Datas.Update.APerfs().then(SideMenu.Datas.Update.Standings).then(() => {
  678. if (Object.keys(SideMenu.Datas.APerfs).length === 0) {
  679. disabled();
  680. AddAlert('APerfのデータが提供されていません');
  681. return;
  682. }
  683. if (SideMenu.Datas.Standings.Fixed) {
  684. SideMenu.DataBase.SetData('APerfs', contestScreenName, SideMenu.Datas.APerfs);
  685. SideMenu.DataBase.SetData('Standings', contestScreenName, SideMenu.Datas.Standings);
  686. }
  687. CalcActivePerf();
  688. if (isStandingsPage) {
  689. updateResultsData();
  690. addPerfToStandings();
  691. }
  692. drawPredictor();
  693. enabled();
  694. AddAlert(\`最終更新 : \${moment().format('HH:mm:ss')}\`);
  695. }).fail(() => {
  696. disabled();
  697. AddAlert('データの読み込みに失敗しました');
  698. });
  699. }
  700. \/\/ActivePerfの再計算
  701. function CalcActivePerf() {
  702. activePerf = [];
  703. var isSomebodyRated = false;
  704. \/\/Perf計算時に使うパフォ(Ratedオンリー)
  705. SideMenu.Datas.Standings.StandingsData.forEach(function (element) {
  706. if (element.IsRated && element.TotalResult.Count !== 0) {
  707. isSomebodyRated = true;
  708. if (!(SideMenu.Datas.APerfs[element.UserScreenName])) {
  709. activePerf.push(defaultAPerf);
  710. }
  711. else {
  712. activePerf.push(SideMenu.Datas.APerfs[element.UserScreenName])
  713. }
  714. }
  715. });
  716. if (!isSomebodyRated) {
  717. SideMenu.Datas.Standings.Fixed = false;
  718. \/\/元はRatedだったと推測できる場合、通常のRatedと同じような扱い
  719. activePerf = [];
  720. for (var i = 0; i < SideMenu.Datas.Standings.StandingsData.length; i++) {
  721. var element = SideMenu.Datas.Standings.StandingsData[i];
  722. if (element.OldRating >= ratedLimit || element.TotalResult.Count === 0) continue;
  723. SideMenu.Datas.Standings.StandingsData[i].IsRated = true;
  724. if (!(SideMenu.Datas.APerfs[element.UserScreenName])) {
  725. activePerf.push(defaultAPerf);
  726. continue;
  727. }
  728. activePerf.push(SideMenu.Datas.APerfs[element.UserScreenName]);
  729. }
  730. }
  731. }
  732. \/\/フォームを更新
  733. function drawPredictor() {
  734. switch (lastUpdated) {
  735. case 0:
  736. UpdatePredictorFromRank();
  737. break;
  738. case 1:
  739. UpdatePredictorFromPerf();
  740. break;
  741. case 2:
  742. UpdatePredictorFromRate();
  743. break;
  744. }
  745. function UpdatePredictorFromRank() {
  746. var rank = \$("#predictor-input-rank").val();
  747. var perf = getPerf(rank);
  748. var rate = getRate(perf);
  749. lastUpdated = 0;
  750. UpdatePredictor(rank, perf, rate);
  751. }
  752. function UpdatePredictorFromPerf() {
  753. var perf = \$("#predictor-input-perf").val();
  754. var upper = 16384
  755. var lower = 0
  756. while (upper - lower > 0.125) {
  757. if (perf > getPerf(lower + (upper - lower) \/ 2)) upper -= (upper - lower) \/ 2
  758. else lower += (upper - lower) \/ 2
  759. }
  760. lastUpdated = 1
  761. var rank = lower + (upper - lower) \/ 2;
  762. var rate = getRate(perf)
  763. UpdatePredictor(rank, perf, rate)
  764. }
  765. function UpdatePredictorFromRate() {
  766. var rate = \$("#predictor-input-rate").val();
  767. var upper = 16384
  768. var lower = 0
  769. while (upper - lower > 0.125) {
  770. if (rate < getRate(lower + (upper - lower) \/ 2)) upper -= (upper - lower) \/ 2
  771. else lower += (upper - lower) \/ 2
  772. }
  773. lastUpdated = 2
  774. var perf = lower + (upper - lower) \/ 2;
  775. upper = 16384
  776. lower = 0
  777. while (upper - lower > 0.125) {
  778. if (perf > getPerf(lower + (upper - lower) \/ 2)) upper -= (upper - lower) \/ 2
  779. else lower += (upper - lower) \/ 2
  780. }
  781. var rank = lower + (upper - lower) \/ 2;
  782. UpdatePredictor(rank, perf, rate)
  783. }
  784. function UpdatePredictor(rank, perf, rate) {
  785. \$("#predictor-input-rank").val(round(rank))
  786. \$("#predictor-input-perf").val(round(perf))
  787. \$("#predictor-input-rate").val(round(rate))
  788. updatePredictorTweetBtn()
  789. function round(val) {
  790. return Math.round(val * 100) \/ 100;
  791. }
  792. }
  793. \/\/ツイートボタンを更新する
  794. function updatePredictorTweetBtn() {
  795. var tweetStr =
  796. \`Rated内順位: \${\$("#predictor-input-rank").val()}位%0A
  797. パフォーマンス: \${\$("#predictor-input-perf").val()}%0A
  798. レート: \${\$("#predictor-input-rate").val()}\`
  799. \$('#predictor-tweet').attr("href", \`https:\/\/twitter.com\/intent\/tweet?text=\${tweetStr}\`)
  800. }
  801. }
  802. \/\/最終更新などの要素を追加する
  803. function AddAlert(content) {
  804. \$("#predictor-alert").html(\`<h5 class='sidemenu-txt'>\${content}<\/h5>\`);
  805. }
  806. \/\/要素のDisableadを外す
  807. function enabled() {
  808. \$('#predictor-reload').button('reset');
  809. predictorElements.forEach(element => {
  810. \$(\`#\${element}\`).removeAttr("disabled");
  811. });
  812. }
  813. \/\/要素にDisableadをつける
  814. function disabled() {
  815. \$('#predictor-reload').button('reset');
  816. predictorElements.forEach(element => {
  817. \$(\`#\${element}\`).attr("disabled", true);
  818. });
  819. }
  820. \/\/全員の結果データを更新する
  821. function updateResultsData() {
  822. eachParticipationResults = {};
  823. const IsFixed = SideMenu.Datas.Standings.Fixed;
  824. \/\/タイの人を入れる(順位が変わったら描画→リストを空に)
  825. var tiedList = [];
  826. var rank = 1;
  827. var lastRank = 0;
  828. var ratedCount = 0;
  829. var maxPerf = ratedLimit === Infinity ? getPerf(1) : ratedLimit + 400;
  830. var currentPerf = maxPerf - 0.5;
  831. var rankVal = calcRankVal(currentPerf);
  832. \/\/全員回す
  833. SideMenu.Datas.Standings.StandingsData.forEach(function (element) {
  834. if (lastRank !== element.Rank) {
  835. addRow();
  836. rank += ratedCount;
  837. ratedCount = 0;
  838. tiedList = [];
  839. }
  840. tiedList.push(element);
  841. lastRank = element.Rank;
  842. if (element.IsRated && element.TotalResult.Count !== 0) ratedCount++;
  843. });
  844. \/\/最後に更新してあげる
  845. addRow();
  846. \/\/タイリストの人全員行追加
  847. function addRow() {
  848. var fixRank = rank + Math.max(0, ratedCount - 1) \/ 2;
  849. while (rankVal < fixRank - 0.5 && currentPerf >= -8192) {
  850. currentPerf--;
  851. rankVal = calcRankVal(currentPerf);
  852. }
  853. tiedList.forEach(e => {
  854. var isRated = e.IsRated && e.TotalResult.Count !== 0;
  855. var isSubmitted = e.TotalResult.Count !== 0;
  856. var matches = e.Competitions - (IsFixed && isRated ? 1 : 0);
  857. var perf = currentPerf + 0.5;
  858. var oldRate = (IsFixed && isSubmitted ? e.OldRating : e.Rating);
  859. var newRate = (IsFixed ? e.Rating : Math.floor(positivize_rating(matches !== 0 ? calc_rating_from_last(oldRate, perf, matches) : perf - 1200)));
  860. eachParticipationResults[e.UserScreenName] = { perf: perf, oldRate: oldRate, newRate: newRate, isRated: isRated, isSubmitted: isSubmitted };
  861. });
  862. }
  863. }
  864. \/\/結果データを順位表に追加する
  865. function addPerfToStandings() {
  866. if (!isStandingsPage) return;
  867. if (!isAlreadyAppendRowToStandings) {
  868. (new MutationObserver(() => { addPerfToStandings(); })).observe(document.getElementById('standings-tbody'), { childList: true });
  869. \$('thead > tr').append('<th class="standings-result-th" style="width:84px;min-width:84px;">perf<\/th><th class="standings-result-th" style="width:168px;min-width:168px;">レート変化<\/th>');
  870. isAlreadyAppendRowToStandings = true;
  871. }
  872. \$('#standings-tbody > tr').each((index, elem) => {
  873. var userName = \$('.standings-username .username', elem).text();
  874. var perfArr = eachParticipationResults[userName];
  875. if (!perfArr) {
  876. \$(elem).append(\`<td class="standings-result">-<\/td>\`);
  877. \$(elem).append(\`<td class="standings-result">-<\/td>\`);
  878. return;
  879. }
  880. var perf = perfArr.isSubmitted ? ratingSpan(perfArr.perf) : '<span class="user-unrated">-<\/span>';
  881. var oldRate = perfArr.oldRate;
  882. var newRate = perfArr.newRate;
  883. var IsRated = perfArr.isRated;
  884. \$(elem).append(\`<td class="standings-result">\${ratingSpan(perf)}<\/td>\`);
  885. \$(elem).append(\`<td class="standings-result">\${getRatingChangeStr(oldRate,newRate)}<\/td>\`);
  886. function getRatingChangeStr(oldRate, newRate) {
  887. return IsRated ? \`\${ratingSpan(oldRate)} -> \${ratingSpan(newRate)}(\${(newRate >= oldRate ? '+' : '')}\${newRate - oldRate})\` : \`\${ratingSpan(oldRate)}(unrated)\`;
  888. }
  889. function ratingSpan(rate) {
  890. return \`<span class="user-\${SideMenu.GetColor(rate)}">\${rate}<\/span>\`;
  891. }
  892. });
  893. }
  894. })();`;
  895. var style =
  896. ``;
  897. var dom =
  898. `<div id="predictor-alert" class="row"><h5 class='sidemenu-txt'>順位表読み込み中…<\/h5><\/div>
  899. <div id="predictor-data" class="row">
  900. <div class="input-group col-xs-offset-1 col-xs-10">
  901. <span class="input-group-addon">順位<span class="glyphicon glyphicon-question-sign" aria-hidden="true" data-html="true" data-toggle="tooltip" data-placement="right" title="" data-original-title="Rated内の順位です。複数人同順位の際は人数を加味します(5位が4人居たら6.5位として計算)"><\/span><\/span>
  902. <input class="form-control" id="predictor-input-rank">
  903. <span class="input-group-addon">位<\/span>
  904. <\/div>
  905. <div class="input-group col-xs-offset-1 col-xs-10">
  906. <span class="input-group-addon">パフォーマンス<\/span>
  907. <input class="form-control" id="predictor-input-perf">
  908. <\/div>
  909. <div class="input-group col-xs-offset-1 col-xs-10">
  910. <span class="input-group-addon">レーティング<\/span>
  911. <input class="form-control" id="predictor-input-rate">
  912. <\/div>
  913. <\/div>
  914. <div class="row">
  915. <div class="btn-group col-xs-offset-1">
  916. <button class="btn btn-default" id="predictor-current">現在の順位<\/button>
  917. <button type="button" class="btn btn-primary" id="predictor-reload" data-loading-text="更新中…">更新<\/button>
  918. <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>
  919. <!--<button class="btn btn-default" id="predictor-solved" disabled>現問題AC後<\/button>-->
  920. <\/div>
  921. <\/div>`;
  922. return `${dom}
  923. <script>${js}</script>
  924. <style>${style}</style>`;
  925. }
  926. });
  927.  
  928.  
  929.  
  930. SideMenu.ViewOrder.forEach(async (elem) => {
  931. await SideMenu.Elements[elem]();
  932. });