Lichess: Training: Stats for Current Run

When doing puzzles, this will show you your stats

  1. // ==UserScript==
  2. // @name Lichess: Training: Stats for Current Run
  3. // @version 1.1.0
  4. // @author pedro-mass
  5. // @copyright 2024, Pedro Mass (https://github.com/pedro-mass)
  6. // @description When doing puzzles, this will show you your stats
  7. // @grant none
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=lichess.org
  9. // @license GNU GPLv3
  10. // @match https://lichess.org/training/*
  11. // @match https://lichess.org/training
  12. // @namespace http://tampermonkey.net/
  13. // @run-at document-idle
  14. // ==/UserScript==
  15.  
  16. const ids = {
  17. stats: "pm-stats",
  18. };
  19. const selectors = {
  20. results: ".result-empty",
  21. stats: `#${ids.stats}`,
  22. puzzleHolder: ".puzzle__session",
  23. };
  24. const constants = {
  25. failure: "result-false",
  26. success: "result-true",
  27. };
  28.  
  29. waitForElement(document, selectors.results).then(() => {
  30. run();
  31. watchElement(document.querySelector(selectors.puzzleHolder), (changes) => {
  32. if (wasTextChange(changes)) return;
  33.  
  34. run();
  35. });
  36. });
  37.  
  38. // ------------------
  39. // helper functions
  40. // ------------------
  41.  
  42. /**
  43. * @param {MutationRecord[]} changes
  44. */
  45. function wasTextChange(changes) {
  46. if (!changes || changes.length === 0) return;
  47.  
  48. const firstChange = changes[0];
  49.  
  50. return (
  51. firstChange.target.id === ids.stats && firstChange.type === "childList"
  52. );
  53. }
  54.  
  55. // src: https://stackoverflow.com/a/47406751/2911615
  56. function waitForElement(root, selector) {
  57. return new Promise((resolve, _reject) => {
  58. new MutationObserver(check).observe(root, {
  59. childList: true,
  60. subtree: true,
  61. });
  62. function check(_changes, observer) {
  63. let element = root.querySelector(selector);
  64. if (element) {
  65. observer.disconnect();
  66. resolve(element);
  67. }
  68. }
  69. });
  70. }
  71.  
  72. function watchElement(root = document, onChange) {
  73. new MutationObserver(onChange).observe(root, {
  74. childList: true,
  75. subtree: true,
  76. });
  77. }
  78.  
  79. function run() {
  80. const statsElem = getStatsElem();
  81. displayFailures(statsElem);
  82. let showFailures = true;
  83.  
  84. statsElem.addEventListener("click", function flipDisplay() {
  85. showFailures = !showFailures;
  86.  
  87. showFailures ? displayFailures(statsElem) : displaySuccesses(statsElem);
  88. });
  89. }
  90.  
  91. function getResults() {
  92. return Array.from(document.querySelectorAll(selectors.results));
  93. }
  94.  
  95. function getStatsElem() {
  96. return document.querySelector(selectors.stats) ?? createStatsElem();
  97. }
  98.  
  99. function createStatsElem() {
  100. const statsElem = document.createElement("div");
  101. statsElem.id = ids.stats;
  102. document.querySelector(selectors.puzzleHolder).appendChild(statsElem);
  103.  
  104. return statsElem;
  105. }
  106.  
  107. function getStats() {
  108. const results = getResults();
  109. const failures = results.filter((x) =>
  110. Array.from(x.classList).includes(constants.failure)
  111. );
  112. const successes = results.filter((x) =>
  113. Array.from(x.classList).includes(constants.success)
  114. );
  115.  
  116. return {
  117. total: results.length,
  118. failures: failures.length,
  119. successes: successes.length,
  120. };
  121. }
  122.  
  123. function displayFailures(elem) {
  124. const { total, failures } = getStats();
  125.  
  126. elem.textContent = `${failures} / ${total} failures`;
  127. }
  128.  
  129. function displaySuccesses(elem) {
  130. const { total, successes } = getStats();
  131.  
  132. elem.textContent = `${successes} / ${total} successes`;
  133. }