Chess.com Stockfish Bot

Chess.com Stockfish Bot with Enhanced Auto-Match using Stockfish Online API

目前为 2025-03-21 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Chess.com Stockfish Bot
  3. // @namespace BottleOrg Scripts
  4. // @version 1.6.2.19
  5. // @description Chess.com Stockfish Bot with Enhanced Auto-Match using Stockfish Online API
  6. // @author [REDACTED]
  7. // @license Chess.com Bot/Cheat by [REDACTED]
  8. // @match https://www.chess.com/play/*
  9. // @match https://www.chess.com/game/*
  10. // @match https://www.chess.com/puzzles/*
  11. // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // @grant GM_xmlhttpRequest
  15. // @grant GM_getResourceText
  16. // @grant GM_registerMenuCommand
  17. // @require https://greasyfork.org/scripts/445697/code/index.js
  18. // @require https://code.jquery.com/jquery-3.6.0.min.js
  19. // @run-at document-start
  20. // ==/UserScript==
  21.  
  22. const currentVersion = '1.6.2.19';
  23. const scriptURL = 'https://greasyfork.org/en/scripts/526240-chess-com-stockfish-bot';
  24. const stockfishAPI_URI = "https://stockfish.online/api/s/v2.php";
  25.  
  26. // Function to check if the Stockfish API domain is accessible
  27. function checkStockfishDomainAccess(callback) {
  28. console.log("Checking if Stockfish API domain is accessible...");
  29. const testFen = "rnbqkbnr/pppppppp/5n2/8/8/5N2/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
  30. const testDepth = 1;
  31. const testUrl = `${stockfishAPI_URI}?fen=${encodeURIComponent(testFen)}&depth=${testDepth}`;
  32.  
  33. GM_xmlhttpRequest({
  34. method: "GET",
  35. url: testUrl,
  36. onload: function(response) {
  37. if (response.status === 200) {
  38. console.log("Stockfish API domain is accessible.");
  39. callback(true);
  40. } else {
  41. console.error("Stockfish API request failed with status:", response.status);
  42. callback(false);
  43. }
  44. },
  45. onerror: function(error) {
  46. console.error("Stockfish API request blocked or failed:", error);
  47. // Check if the error indicates the request was blocked (e.g., by an ad-blocker)
  48. if (error && error.statusText && error.statusText.includes("net::ERR_BLOCKED_BY_CLIENT")) {
  49. alert("Allow domain request to stockfish.online for the script to be able to work!");
  50. } else {
  51. alert("Failed to connect to Stockfish API. Please check your internet connection or allow requests to stockfish.online.");
  52. }
  53. callback(false);
  54. },
  55. timeout: 5000 // Set a timeout of 5 seconds for the test request
  56. });
  57. }
  58.  
  59. function checkForUpdate() {
  60. console.log("Checking for script updates...");
  61. GM_xmlhttpRequest({
  62. method: "GET",
  63. url: scriptURL,
  64. onload: function(response) {
  65. if (response.status === 200) {
  66. const html = response.responseText;
  67. const versionMatch = html.match(/@version\s+([\d.]+)/);
  68. if (versionMatch && versionMatch[1]) {
  69. const latestVersion = versionMatch[1];
  70. console.log("Latest version found:", latestVersion);
  71. if (compareVersions(latestVersion, currentVersion) > 0) {
  72. const message = `New Version: ${latestVersion} has been uploaded. Would you like me to take you there or continue with old version ${currentVersion}? (Not recommended for stability)`;
  73. if (confirm(message)) {
  74. window.location.href = scriptURL;
  75. } else {
  76. console.log("User chose to continue with old version.");
  77. }
  78. } else {
  79. console.log("No newer version available.");
  80. }
  81. } else {
  82. console.error("Could not find version in Greasy Fork page.");
  83. }
  84. } else {
  85. console.error("Failed to fetch script page:", response.status);
  86. }
  87. },
  88. onerror: function(error) {
  89. console.error("Error checking for update:", error);
  90. }
  91. });
  92. }
  93.  
  94. function compareVersions(v1, v2) {
  95. const parts1 = v1.split('.').map(Number);
  96. const parts2 = v2.split('.').map(Number);
  97. for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
  98. const p1 = parts1[i] || 0;
  99. const p2 = parts2[i] || 0;
  100. if (p1 > p2) return 1;
  101. if (p1 < p2) return -1;
  102. }
  103. return 0;
  104. }
  105.  
  106. function main() {
  107. var myVars = document.myVars = { autoMove: false, autoRun: false, autoMatch: false, delay: 0.1, hasAutoMatched: false, gameEnded: false, stockfishAccessible: false };
  108. var myFunctions = document.myFunctions = {};
  109. var currentStockfishVersion = "Stockfish API";
  110. var uiElementsLoaded = false;
  111.  
  112. // Check Stockfish API access before proceeding
  113. checkStockfishDomainAccess(function(isAccessible) {
  114. myVars.stockfishAccessible = isAccessible;
  115. if (!isAccessible) {
  116. console.error("Stockfish API is not accessible. Script functionality will be limited.");
  117. return; // Stop script execution if Stockfish API is not accessible
  118. }
  119. proceedWithMainLogic();
  120. });
  121.  
  122. function proceedWithMainLogic() {
  123. var stop_b = 0, stop_w = 0, s_br = 0, s_br2 = 0, s_wr = 0, s_wr2 = 0;
  124.  
  125. myFunctions.rescan = function() {
  126. console.log("Rescanning board...");
  127. var boardElement = document.querySelector('chess-board, wc-chess-board');
  128. if (!boardElement) {
  129. console.warn("No board element found. Using default FEN.");
  130. return "rnbqkbnr/pppppppp/5n2/8/8/5N2/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
  131. }
  132. var pieces = $(boardElement).find(".piece").map(function() { return this.className; }).get();
  133. if (!pieces.length) {
  134. console.warn("No pieces found. Using default FEN.");
  135. return "rnbqkbnr/pppppppp/5n2/8/8/5N2/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
  136. }
  137. var boardArray = Array(64).fill('');
  138. pieces.forEach(piece => {
  139. var classes = piece.split(' ');
  140. var squareClass = classes.find(c => c.startsWith('square-'));
  141. var pieceClass = classes.find(c => /^[wb][prnbqk]$/.test(c));
  142. if (squareClass && pieceClass) {
  143. var squareNum = squareClass.replace('square-', '');
  144. var file = parseInt(squareNum[0]) - 1;
  145. var rank = parseInt(squareNum[1]) - 1;
  146. var square = (7 - rank) * 8 + file;
  147. if (square >= 0 && square < 64) {
  148. var pieceChar = {'wp': 'P', 'bp': 'p', 'wr': 'R', 'br': 'r', 'wn': 'N', 'bn': 'n',
  149. 'wb': 'B', 'bb': 'b', 'wq': 'Q', 'bq': 'q', 'wk': 'K', 'bk': 'k'}[pieceClass];
  150. boardArray[square] = pieceChar;
  151. }
  152. }
  153. });
  154. var fen = '';
  155. for (var i = 0; i < 64; i++) {
  156. if (i % 8 === 0 && i > 0) fen += '/';
  157. var piece = boardArray[i];
  158. if (!piece) {
  159. var emptyCount = 1;
  160. while (i + 1 < 64 && !boardArray[i + 1] && (i + 1) % 8 !== 0) {
  161. emptyCount++;
  162. i++;
  163. }
  164. fen += emptyCount;
  165. } else {
  166. fen += piece;
  167. }
  168. }
  169. var turn = $('.coordinates').children().first().text() === "1" ? 'b' : 'w';
  170. var castling = (stop_w ? '' : 'KQ') + (stop_b ? '' : 'kq') || '-';
  171. fen += ` ${turn} ${castling} - 0 1`;
  172. console.log("Generated FEN:", fen);
  173. return fen;
  174. };
  175.  
  176. myFunctions.color = function(dat) {
  177. console.log("myFunctions.color CALLED with:", dat);
  178. const bestmoveUCI = dat.split(' ')[1];
  179. console.log("Extracted bestmove UCI:", bestmoveUCI);
  180. if (myVars.autoMove) myFunctions.movePiece(bestmoveUCI);
  181. else myFunctions.highlightMove(bestmoveUCI);
  182. isThinking = false;
  183. myFunctions.spinner();
  184. };
  185.  
  186. myFunctions.highlightMove = function(bestmoveUCI) {
  187. var res1 = bestmoveUCI.substring(0, 2), res2 = bestmoveUCI.substring(2, 4);
  188. $(board).prepend(`<div class="highlight square-${res2}" style="background-color: rgb(235, 97, 80); opacity: 0.71;"></div>`)
  189. .children(':first').delay(1800).queue(function() { $(this).remove(); });
  190. $(board).prepend(`<div class="highlight square-${res1}" style="background-color: rgb(235, 97, 80); opacity: 0.71;"></div>`)
  191. .children(':first').delay(1800).queue(function() { $(this).remove(); });
  192. console.log("Highlighted:", bestmoveUCI);
  193. };
  194.  
  195. myFunctions.movePiece = function(bestmoveUCI) {
  196. console.log("movePiece CALLED with:", bestmoveUCI);
  197. if (!board || !board.game) {
  198. console.error("Board or board.game not initialized!");
  199. return;
  200. }
  201. const fromSquare = bestmoveUCI.substring(0, 2);
  202. const toSquare = bestmoveUCI.substring(2, 4);
  203. const legalMoves = board.game.getLegalMoves();
  204. console.log("Legal moves:", legalMoves);
  205. let foundMove = legalMoves.find(move => move.from === fromSquare && move.to === toSquare);
  206. if (foundMove) {
  207. console.log("Executing move:", foundMove);
  208. board.game.move({ ...foundMove, promotion: 'q', animate: true, userGenerated: true });
  209. console.log("Move executed:", bestmoveUCI);
  210. } else {
  211. console.warn("No legal move found for:", bestmoveUCI);
  212. }
  213. };
  214.  
  215. myFunctions.reloadChessEngine = function() { console.log("Reload not needed for API."); };
  216. myFunctions.loadChessEngine = function() {
  217. console.log("Using Stockfish API.");
  218. if (uiElementsLoaded) $('#engineVersionText')[0].innerHTML = "Engine: <strong>Stockfish API</strong>";
  219. };
  220.  
  221. myFunctions.fetchBestMoveFromAPI = function(fen, depth) {
  222. if (!myVars.stockfishAccessible) {
  223. console.error("Stockfish API is not accessible. Cannot fetch best move.");
  224. isThinking = false;
  225. myFunctions.spinner();
  226. return;
  227. }
  228. const apiURL = `${stockfishAPI_URI}?fen=${encodeURIComponent(fen)}&depth=${depth}`;
  229. console.log(`Fetching from: ${apiURL}`);
  230. GM_xmlhttpRequest({
  231. method: "GET",
  232. url: apiURL,
  233. onload: function(response) {
  234. if (response.status === 200) {
  235. try {
  236. const jsonResponse = JSON.parse(response.responseText);
  237. if (jsonResponse.success) {
  238. console.log("API Response:", jsonResponse);
  239. myFunctions.color(jsonResponse.bestmove);
  240. } else {
  241. console.error("API failed:", jsonResponse);
  242. isThinking = false;
  243. myFunctions.spinner();
  244. }
  245. } catch (e) {
  246. console.error("API parse error:", e);
  247. isThinking = false;
  248. myFunctions.spinner();
  249. }
  250. } else {
  251. console.error("API error:", response.status);
  252. isThinking = false;
  253. myFunctions.spinner();
  254. }
  255. },
  256. onerror: function(error) {
  257. console.error("API request error:", error);
  258. isThinking = false;
  259. myFunctions.spinner();
  260. }
  261. });
  262. };
  263.  
  264. myFunctions.startNewGame = function() {
  265. console.log("Starting new game...");
  266. const modalNewGameButton = $('.game-over-modal-content .game-over-buttons-component .cc-button-component:not([aria-label="Rematch"])');
  267. if (modalNewGameButton.length) {
  268. modalNewGameButton[0].click();
  269. console.log("Clicked New <x> min button from game-over modal.");
  270. myVars.hasAutoMatched = true;
  271. return;
  272. }
  273.  
  274. const newGameButton = $('.game-over-buttons-component .cc-button-component:not([aria-label="Rematch"])');
  275. if (newGameButton.length) {
  276. newGameButton[0].click();
  277. console.log("Clicked New <x> min button from game-over.");
  278. myVars.hasAutoMatched = true;
  279. return;
  280. }
  281.  
  282. const newGameTab = $('[data-tab="newGame"]');
  283. if (newGameTab.length) {
  284. newGameTab[0].click();
  285. console.log("Clicked New Game tab.");
  286. setTimeout(() => {
  287. const playButton = $('.cc-button-component.cc-button-primary.cc-button-xx-large.cc-button-full');
  288. if (playButton.length) {
  289. playButton[0].click();
  290. console.log("Clicked Play button.");
  291. myVars.hasAutoMatched = true;
  292. // Wait for the modal to appear and then attempt to click the guest button
  293. setTimeout(() => {
  294. attemptGuestButtonClick();
  295. }, 2000); // Increased delay to ensure modal loads
  296. } else {
  297. console.error("Play button not found!");
  298. }
  299. }, 500);
  300. } else {
  301. console.error("New Game tab not found!");
  302. }
  303. };
  304.  
  305. myFunctions.declineRematch = function() {
  306. const declineButton = $('.cc-button-component.cc-button-secondary[aria-label="Decline"], .cc-button-component.cc-button-secondary:contains("Decline")');
  307. if (declineButton.length) {
  308. declineButton[0].click();
  309. console.log("Declined rematch.");
  310. return true;
  311. } else {
  312. console.log("No rematch decline button found.");
  313. return false;
  314. }
  315. };
  316.  
  317. var lastValue = 11, MAX_DEPTH = 15, MIN_DEPTH = 1;
  318.  
  319. myFunctions.runChessEngine = function(depth) {
  320. if (!myVars.stockfishAccessible) {
  321. console.error("Stockfish API is not accessible. Cannot run chess engine.");
  322. return;
  323. }
  324. depth = Math.max(MIN_DEPTH, Math.min(MAX_DEPTH, depth));
  325. var fen = myFunctions.rescan();
  326. console.log(`Analyzing FEN: ${fen}, Depth: ${depth}`);
  327. isThinking = true;
  328. myFunctions.spinner();
  329. myFunctions.fetchBestMoveFromAPI(fen, depth);
  330. lastValue = depth;
  331. updateDepthDisplay();
  332. };
  333.  
  334. function updateDepthDisplay() {
  335. if (uiElementsLoaded && $('#depthText')[0]) $('#depthText')[0].innerHTML = `Depth: <strong>${lastValue}</strong>`;
  336. }
  337.  
  338. myFunctions.incrementDepth = function(delta) {
  339. lastValue = Math.max(MIN_DEPTH, Math.min(MAX_DEPTH, lastValue + delta));
  340. updateDepthDisplay();
  341. };
  342.  
  343. myFunctions.autoRun = function() {
  344. if (board && board.game && board.game.getTurn() === board.game.getPlayingAs()) {
  345. myFunctions.runChessEngine(lastValue);
  346. }
  347. };
  348.  
  349. document.onkeydown = function(e) {
  350. switch (e.keyCode) {
  351. case 81: myFunctions.runChessEngine(1); break; // Q
  352. case 87: myFunctions.runChessEngine(2); break; // W
  353. case 69: myFunctions.runChessEngine(3); break; // E
  354. case 82: myFunctions.runChessEngine(4); break; // R
  355. case 84: myFunctions.runChessEngine(5); break; // T
  356. case 89: myFunctions.runChessEngine(6); break; // Y
  357. case 85: myFunctions.runChessEngine(7); break; // U
  358. case 73: myFunctions.runChessEngine(8); break; // I
  359. case 79: myFunctions.runChessEngine(9); break; // O
  360. case 80: myFunctions.runChessEngine(10); break; // P
  361. case 65: myFunctions.runChessEngine(11); break; // A
  362. case 83: myFunctions.runChessEngine(12); break; // S
  363. case 68: myFunctions.runChessEngine(13); break; // D
  364. case 70: myFunctions.runChessEngine(14); break; // F
  365. case 71: myFunctions.runChessEngine(15); break; // G
  366. case 187: myFunctions.incrementDepth(1); break; // +
  367. case 189: myFunctions.incrementDepth(-1); break;// -
  368. }
  369. };
  370.  
  371. myFunctions.spinner = function() {
  372. if (uiElementsLoaded && $('#overlay')[0]) {
  373. $('#overlay')[0].style.display = isThinking ? 'block' : 'none';
  374. }
  375. };
  376.  
  377. let dynamicStyles = null;
  378. function addAnimation(body) {
  379. if (!dynamicStyles) {
  380. dynamicStyles = document.createElement('style');
  381. document.head.appendChild(dynamicStyles);
  382. }
  383. dynamicStyles.sheet.insertRule(body, dynamicStyles.length);
  384. }
  385.  
  386. var loaded = false;
  387. myFunctions.loadEx = function() {
  388. try {
  389. console.log("Attempting to load UI...");
  390. board = document.querySelector('chess-board, wc-chess-board');
  391. var div = document.createElement('div');
  392. div.innerHTML = `
  393. <div style="margin: 8px; padding: 10px; background: white; border: 1px solid #000; border-radius: 5px;">
  394. <p id="depthText">Depth: <strong>${lastValue}</strong></p>
  395. <button id="depthMinus">-</button>
  396. <button id="depthPlus">+</button>
  397. <p style="font-size: 12px;">Keys: Q-G (1-15), +/-</p>
  398. <p id="engineVersionText">Engine: <strong>Stockfish API</strong></p>
  399. <label><input type="checkbox" id="autoRun"> Auto Run</label><br>
  400. <label><input type="checkbox" id="autoMove"> Auto Move</label><br>
  401. <label><input type="checkbox" id="autoMatch"> Auto-Match</label><br>
  402. <label>Min Delay (s): <input type="number" id="timeDelayMin" min="0.1" value="0.1" step="0.1" style="width: 60px;"></label><br>
  403. <label>Max Delay (s): <input type="number" id="timeDelayMax" min="0.1" value="1" step="0.1" style="width: 60px;"></label><br>
  404. <p style="color: blue; font-size: 12px; margin-top: 5px;">Script is made for guest account since Stockfish makes unhuman moves!</p>
  405. </div>`;
  406. div.style.cssText = 'position: fixed; top: 10px; right: 10px; z-index: 10000;';
  407. document.body.appendChild(div);
  408.  
  409. setTimeout(() => {
  410. $('#depthPlus').off('click').on('click', () => myFunctions.incrementDepth(1));
  411. $('#depthMinus').off('click').on('click', () => myFunctions.incrementDepth(-1));
  412. $('#autoMatch').on('change', () => {
  413. myVars.autoMatch = $('#autoMatch')[0].checked;
  414. if (myVars.autoMatch && !myVars.hasAutoMatched) {
  415. myFunctions.startNewGame();
  416. }
  417. });
  418. console.log("Event listeners bound.");
  419. }, 100);
  420.  
  421. var spinCont = document.createElement('div');
  422. spinCont.id = 'overlay';
  423. spinCont.style.cssText = 'display: none; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);';
  424. div.appendChild(spinCont);
  425. var spinr = document.createElement('div');
  426. spinr.style.cssText = "height: 64px; width: 64px; animation: rotate 0.8s infinite linear; border: 5px solid firebrick; border-right-color: transparent; border-radius: 50%;";
  427. spinCont.appendChild(spinr);
  428. addAnimation(`@keyframes rotate { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }`);
  429.  
  430. loaded = true;
  431. uiElementsLoaded = true;
  432. console.log("UI loaded successfully.");
  433. myFunctions.loadChessEngine();
  434. checkForUpdate();
  435. } catch (error) {
  436. console.error("loadEx error:", error);
  437. }
  438. };
  439.  
  440. function other(delay) {
  441. var endTime = Date.now() + delay;
  442. var timer = setInterval(() => {
  443. if (Date.now() >= endTime) {
  444. myFunctions.autoRun();
  445. canGo = true;
  446. clearInterval(timer);
  447. }
  448. }, 10);
  449. }
  450.  
  451. // Function to attempt clicking the guest button with retries
  452. function attemptGuestButtonClick(attempts = 10, delay = 1000) {
  453. if (attempts <= 0) {
  454. console.error("Failed to click 'Play as a Guest' button after all retries.");
  455. return;
  456. }
  457.  
  458. // Check if the modal is present
  459. const modal = $('.authentication-modal-content');
  460. if (!modal.length) {
  461. console.log("Modal not found, attempts left:", attempts);
  462. setTimeout(() => attemptGuestButtonClick(attempts - 1, delay), delay);
  463. return;
  464. }
  465.  
  466. const guestButton = $('#guest-button.authentication-intro-guest');
  467. if (guestButton.length && !myVars.hasAutoMatched) {
  468. console.log("Found 'Play as a Guest' button:", guestButton[0]);
  469. guestButton[0].click(); // Use jQuery's click method
  470. console.log("Clicked 'Play as a Guest' button.");
  471. setTimeout(() => {
  472. const playButton = $('.cc-button-component.cc-button-primary.cc-button-xx-large.cc-button-full');
  473. if (playButton.length) {
  474. playButton[0].click();
  475. console.log("Clicked Play button after guest prompt.");
  476. myVars.hasAutoMatched = true;
  477. } else {
  478. console.error("Play button not found after guest prompt!");
  479. }
  480. }, 1500); // Increased delay to 1500ms to ensure page transition
  481. } else {
  482. console.log("Guest button not found or already clicked, attempts left:", attempts);
  483. setTimeout(() => attemptGuestButtonClick(attempts - 1, delay), delay);
  484. }
  485. }
  486.  
  487. const waitForChessBoard = setInterval(() => {
  488. if (!loaded) {
  489. myFunctions.loadEx();
  490. } else {
  491. clearInterval(waitForChessBoard);
  492. if (!board) board = document.querySelector('chess-board, wc-chess-board');
  493. myVars.autoRun = $('#autoRun')[0].checked;
  494. myVars.autoMove = $('#autoMove')[0].checked;
  495. myVars.autoMatch = $('#autoMatch')[0].checked;
  496. let minDel = parseFloat($('#timeDelayMin')[0].value) || 0.1;
  497. let maxDel = parseFloat($('#timeDelayMax')[0].value) || 1;
  498. myVars.delay = Math.random() * (maxDel - minDel) + minDel;
  499. myFunctions.spinner();
  500. myTurn = board && board.game && board.game.getTurn() === board.game.getPlayingAs();
  501. updateDepthDisplay();
  502.  
  503. const gameOver = document.querySelector('.game-over-message-component') || document.querySelector('.game-result');
  504. if (gameOver && !myVars.gameEnded) {
  505. console.log("Game ended detected (chat or result).");
  506. myVars.gameEnded = true;
  507. if (myVars.autoMatch) {
  508. myFunctions.declineRematch();
  509. setTimeout(() => {
  510. myVars.hasAutoMatched = false;
  511. myFunctions.startNewGame();
  512. }, 1000);
  513. }
  514. } else if (!gameOver && myVars.gameEnded) {
  515. myVars.gameEnded = false;
  516. }
  517.  
  518. const gameOverModal = $('.game-over-modal-content');
  519. if (myVars.autoMatch && gameOverModal.length && !myVars.hasAutoMatched) {
  520. console.log("Game over modal detected.");
  521. const newGameButton = gameOverModal.find('.game-over-buttons-component .cc-button-component:not([aria-label="Rematch"])');
  522. if (newGameButton.length) {
  523. newGameButton[0].click();
  524. console.log("Clicked New <x> min button from game-over modal in interval.");
  525. myVars.hasAutoMatched = true;
  526. } else {
  527. console.error("New <x> min button not found in game-over modal!");
  528. }
  529. }
  530.  
  531. if (myVars.autoRun && canGo && !isThinking && myTurn) {
  532. canGo = false;
  533. other(myVars.delay * 1000);
  534. }
  535. }
  536. }, 500);
  537.  
  538. setTimeout(() => {
  539. if (!loaded) myFunctions.loadEx();
  540. }, 2000);
  541. }
  542. }
  543.  
  544. var isThinking = false, canGo = true, myTurn = false, board;
  545.  
  546. window.addEventListener("load", () => main());