Connect 4 AI for papergames

Adds an autonomous AI player to Connect 4 on papergames.io with Python mouse control and multiple AI APIs

بۇ قوليازمىنى قاچىلاش؟
ئاپتورنىڭ تەۋسىيەلىگەن قوليازمىسى

سىز بەلكىم Tic Tac Toe AI for papergames نى ياقتۇرۇشىڭىز مۇمكىن.

بۇ قوليازمىنى قاچىلاش
  1. // ==UserScript==
  2. // @name Connect 4 AI for papergames
  3. // @namespace https://github.com/longkidkoolstar
  4. // @version 0.2.5
  5. // @description Adds an autonomous AI player to Connect 4 on papergames.io with Python mouse control and multiple AI APIs
  6. // @author longkidkoolstar
  7. // @icon https://th.bing.com/th/id/R.2ea02f33df030351e0ea9bd6df0db744?rik=Pnmqtc4WLvL0ow&pid=ImgRaw&r=0
  8. // @require https://code.jquery.com/jquery-3.6.0.min.js
  9. // @match https://papergames.io/*
  10. // @license none
  11. // @grant GM.xmlHttpRequest
  12. // @grant GM.setValue
  13. // @grant GM.getValue
  14. // @connect connect4.gamesolver.org
  15. // @connect kevinalbs.com
  16. // @connect localhost
  17. // ==/UserScript==
  18.  
  19.  
  20. (async function() {
  21. 'use strict';
  22.  
  23. // Configuration variables
  24. const PYTHON_SERVER_URL = 'http://localhost:8765';
  25. const MOVE_DELAY = 1500; // Delay before making a move (ms)
  26. const COOLDOWN_DELAY = 2000; // Cooldown after making a move (ms)
  27. const BOARD_CHECK_INTERVAL = 1000; // How often to check the board (ms)
  28. const RESET_CHECK_INTERVAL = 500; // How often to check for reset buttons (ms)
  29. const SERVER_CHECK_INTERVAL = 10000; // How often to check Python server status (ms)
  30. const SERVER_RETRY_INTERVAL = 3000; // How often to retry connecting to the server when disconnected (ms)
  31. const AUTO_QUEUE_CHECK_INTERVAL = 1000; // How often to check for auto-queue buttons (ms)
  32. const AUTO_QUEUE_ENABLED_DEFAULT = false; // Default state for auto-queue
  33. // State variables
  34. var username = await GM.getValue('username');
  35. var player;
  36. var prevChronometerValue = '';
  37. var moveHistory = [];
  38. var lastBoardState = [];
  39. var aiTurn = false;
  40. var processingMove = false;
  41. var moveCooldown = false;
  42. var pythonServerAvailable = false;
  43. var serverCheckRetryCount = 0;
  44. var autoPlayEnabled = true; // Auto-play is enabled by default
  45. var bestMoveStrategy = 'optimal'; // 'optimal', 'random', 'defensive'
  46. var keyboardControlsEnabled = true; // Enable keyboard controls by default
  47. var selectedAPI = await GM.getValue('selectedAPI', 'gamesolver'); // Default to gamesolver API
  48. var isAutoQueueOn = await GM.getValue('autoQueueEnabled', AUTO_QUEUE_ENABLED_DEFAULT); // Get auto-queue state from storage
  49.  
  50. // If username is not set, prompt the user
  51. if (!username) {
  52. username = prompt('Please enter your Papergames username (case-sensitive):');
  53. await GM.setValue('username', username);
  54. }
  55.  
  56. // Reset all game state variables
  57. function resetVariables() {
  58. player = undefined;
  59. prevChronometerValue = '';
  60. moveHistory = [];
  61. lastBoardState = [];
  62. aiTurn = false;
  63. processingMove = false;
  64. moveCooldown = false;
  65. console.log("Variables reset to default states");
  66. }
  67.  
  68. // Check for UI elements that indicate we should reset game state
  69. function checkForResetButtons() {
  70. var playOnlineButton = document.querySelector("body > app-root > app-navigation > div > div.d-flex.flex-column.h-100.w-100 > main > app-game-landing > div > div > div > div.col-12.col-lg-9.dashboard > div.card.area-buttons.d-flex.justify-content-center.align-items-center.flex-column > button.btn.btn-secondary.btn-lg.position-relative");
  71. var leaveRoomButton = document.querySelector("button.btn-light.ng-tns-c189-7");
  72. var customResetButton = document.querySelector("button.btn.btn-outline-dark.ng-tns-c497539356-18.ng-star-inserted");
  73. if (playOnlineButton || leaveRoomButton || customResetButton) {
  74. resetVariables();
  75. }
  76. // Also reset if we're on certain pages
  77. if (window.location.href.includes("/match-history") ||
  78. window.location.href.includes("/friends") ||
  79. window.location.href.includes("/chat")) {
  80. resetVariables();
  81. }
  82. }
  83.  
  84. // Handle keyboard input for column selection
  85. function setupKeyboardControls() {
  86. document.addEventListener('keydown', function(event) {
  87. // Only process if keyboard controls are enabled and we're on a game page
  88. if (!keyboardControlsEnabled || !document.querySelector(".grid.size6x7")) return;
  89. // Check if the key is a number between 1-7
  90. const column = parseInt(event.key);
  91. if (column >= 1 && column <= 7) {
  92. // Don't process if we're already processing a move or server is unavailable
  93. if (processingMove || !pythonServerAvailable) return;
  94. // Don't capture keyboard input if user is typing in an input field
  95. if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') return;
  96. console.log(`Keyboard input detected: Column ${column}`);
  97. processingMove = true;
  98. clickColumn(column);
  99. // Prevent default action (like scrolling)
  100. event.preventDefault();
  101. }
  102. // Toggle auto-play with 'a' key
  103. if (event.key === 'a' || event.key === 'A') {
  104. toggleAutoPlay();
  105. event.preventDefault();
  106. }
  107. // Toggle keyboard controls with 'k' key
  108. if (event.key === 'k' || event.key === 'K') {
  109. toggleKeyboardControls();
  110. event.preventDefault();
  111. }
  112. // Toggle API with 'h' key (for Human mode)
  113. if (event.key === 'h' || event.key === 'H') {
  114. toggleAPI();
  115. event.preventDefault();
  116. }
  117. // Toggle Auto-Queue with 'q' key
  118. if (event.key === 'q' || event.key === 'Q') {
  119. toggleAutoQueue();
  120. event.preventDefault();
  121. }
  122. });
  123. }
  124. // Toggle keyboard controls
  125. function toggleKeyboardControls() {
  126. keyboardControlsEnabled = !keyboardControlsEnabled;
  127. const $btn = $('#keyboard-controls-toggle');
  128. if (keyboardControlsEnabled) {
  129. $btn.text('Keyboard Controls: ON')
  130. .removeClass('btn-danger')
  131. .addClass('btn-success');
  132. console.log("Keyboard controls enabled");
  133. } else {
  134. $btn.text('Keyboard Controls: OFF')
  135. .removeClass('btn-success')
  136. .addClass('btn-danger');
  137. console.log("Keyboard controls disabled");
  138. }
  139. }
  140.  
  141. // Check if it's the AI's turn to play
  142. function updateBoard() {
  143. if (!autoPlayEnabled) return; // Skip if auto-play is disabled
  144. var profileOpeners = document.querySelectorAll(".text-truncate.cursor-pointer");
  145. var profileOpener = Array.from(profileOpeners).find(opener => opener.textContent.trim() === username);
  146.  
  147. var chronometer = document.querySelector("app-chronometer");
  148. var numberElement;
  149.  
  150. if (profileOpener) {
  151. var profileParent = profileOpener.parentNode;
  152. numberElement = profileOpener.parentNode.querySelectorAll("span")[4];
  153.  
  154. var profileOpenerParent = profileOpener.parentNode.parentNode;
  155. var svgElementDark = profileOpenerParent.querySelector("circle.circle-dark");
  156. var svgElementLight = profileOpenerParent.querySelector("circle.circle-light");
  157.  
  158. if (svgElementDark) {
  159. player = 'R';
  160. } else if (svgElementLight) {
  161. player = 'Y';
  162. }
  163. }
  164.  
  165. var currentElement = chronometer || numberElement;
  166. if (currentElement && currentElement.textContent !== prevChronometerValue && profileOpener) {
  167. prevChronometerValue = currentElement.textContent;
  168. console.log("AI's turn detected. Waiting before making a move...");
  169. aiTurn = true;
  170. setTimeout(() => {
  171. if (!moveCooldown && autoPlayEnabled) {
  172. console.log("Making AI move...");
  173. makeAPIMove();
  174. }
  175. }, MOVE_DELAY);
  176. } else {
  177. aiTurn = false;
  178. }
  179. }
  180.  
  181. // Get the current state of the board
  182. function getBoardState() {
  183. const boardContainer = document.querySelector(".grid.size6x7");
  184. if (!boardContainer) {
  185. console.error("Board container not found");
  186. return [];
  187. }
  188. let boardState = [];
  189. // Iterate over cells in a more flexible way
  190. for (let row = 1; row <= 6; row++) {
  191. let rowState = [];
  192. for (let col = 1; col <= 7; col++) {
  193. // Use a selector that matches the class names correctly
  194. const cellSelector = `.grid-item.cell-${row}-${col}`;
  195. const cell = boardContainer.querySelector(cellSelector);
  196. if (cell) {
  197. // Check the circle class names to determine the cell's state
  198. const circle = cell.querySelector("circle");
  199. if (circle) {
  200. if (circle.classList.contains("circle-dark")) {
  201. rowState.push("R");
  202. } else if (circle.classList.contains("circle-light")) {
  203. rowState.push("Y");
  204. } else {
  205. rowState.push("E");
  206. }
  207. } else {
  208. rowState.push("E");
  209. }
  210. } else {
  211. console.error(`Cell not found: ${cellSelector}`);
  212. rowState.push("E");
  213. }
  214. }
  215. boardState.push(rowState);
  216. }
  217. return boardState;
  218. }
  219. // Detect if a new move has been made
  220. function detectNewMove() {
  221. const currentBoardState = getBoardState();
  222. let newMove = false;
  223. for (let row = 0; row < 6; row++) {
  224. for (let col = 0; col < 7; col++) {
  225. if (lastBoardState[row] && lastBoardState[row][col] === 'E' && currentBoardState[row][col] !== 'E') {
  226. moveHistory.push(col + 1);
  227. newMove = true;
  228. }
  229. }
  230. }
  231. lastBoardState = currentBoardState;
  232. return newMove;
  233. }
  234. // Click on a column using the Python mouse controller
  235. function clickColumn(column) {
  236. console.log(`Requesting Python mouse click on column ${column}`);
  237. if (!pythonServerAvailable) {
  238. console.error("Python server not available. Cannot make move.");
  239. processingMove = false;
  240. return;
  241. }
  242. // Send click request to Python server (0-indexed)
  243. sendClickRequestToPython(column - 1);
  244. }
  245.  
  246. // Send a click request to the Python server using GM.xmlHttpRequest to avoid CORS issues
  247. function sendClickRequestToPython(column) {
  248. GM.xmlHttpRequest({
  249. method: "POST",
  250. url: `${PYTHON_SERVER_URL}/api/click`,
  251. headers: {
  252. "Content-Type": "application/json"
  253. },
  254. data: JSON.stringify({ column: column }),
  255. onload: function(response) {
  256. try {
  257. const data = JSON.parse(response.responseText);
  258. console.log('Python click response:', data);
  259. processingMove = false;
  260. moveCooldown = true;
  261. setTimeout(() => moveCooldown = false, COOLDOWN_DELAY);
  262. } catch (error) {
  263. console.error('Error parsing Python server response:', error);
  264. processingMove = false;
  265. }
  266. },
  267. onerror: function(error) {
  268. console.error('Error communicating with Python server:', error);
  269. processingMove = false;
  270. pythonServerAvailable = false;
  271. updateServerStatusIndicator(false);
  272. // Schedule a retry to check server status
  273. setTimeout(checkPythonServerStatus, SERVER_RETRY_INTERVAL);
  274. }
  275. });
  276. }
  277.  
  278. // Check if the Python server is running using GM.xmlHttpRequest to avoid CORS issues
  279. function checkPythonServerStatus() {
  280. GM.xmlHttpRequest({
  281. method: "GET",
  282. url: `${PYTHON_SERVER_URL}/api/status`,
  283. onload: function(response) {
  284. try {
  285. const data = JSON.parse(response.responseText);
  286. console.log('Python server status:', data);
  287. pythonServerAvailable = true;
  288. serverCheckRetryCount = 0;
  289. updateServerStatusIndicator(true, data.calibrated);
  290. } catch (error) {
  291. console.error('Error parsing Python server status:', error);
  292. pythonServerAvailable = false;
  293. updateServerStatusIndicator(false);
  294. scheduleServerRetry();
  295. }
  296. },
  297. onerror: function(error) {
  298. console.error('Python server not available:', error);
  299. pythonServerAvailable = false;
  300. updateServerStatusIndicator(false);
  301. scheduleServerRetry();
  302. }
  303. });
  304. }
  305. // Schedule a retry to check server status with exponential backoff
  306. function scheduleServerRetry() {
  307. serverCheckRetryCount++;
  308. const delay = Math.min(SERVER_RETRY_INTERVAL * Math.pow(1.5, serverCheckRetryCount - 1), 30000);
  309. console.log(`Scheduling server check retry in ${delay}ms (attempt ${serverCheckRetryCount})`);
  310. setTimeout(checkPythonServerStatus, delay);
  311. }
  312.  
  313. // Make a move using the Connect 4 solver API
  314. function makeAPIMove() {
  315. if (!aiTurn || processingMove || !autoPlayEnabled) return;
  316. // Check if Python server is available before proceeding
  317. if (!pythonServerAvailable) {
  318. console.error("Python server not available. Cannot make move.");
  319. return;
  320. }
  321. processingMove = true;
  322.  
  323. // Use the selected API
  324. if (selectedAPI === 'gamesolver') {
  325. makeGameSolverAPIMove();
  326. } else if (selectedAPI === 'human') {
  327. makeHumanModeAPIMove();
  328. }
  329. }
  330.  
  331. // Make a move using the gamesolver.org API
  332. function makeGameSolverAPIMove() {
  333. detectNewMove();
  334. console.log("Move history:", moveHistory);
  335.  
  336. let pos = moveHistory.join("");
  337. console.log("API position string:", pos);
  338.  
  339. const apiUrl = `https://connect4.gamesolver.org/solve?pos=${pos}`;
  340. console.log("API URL:", apiUrl);
  341.  
  342. GM.xmlHttpRequest({
  343. method: "GET",
  344. url: apiUrl,
  345. onload: function(response) {
  346. console.log("API response received");
  347. try {
  348. const data = JSON.parse(response.responseText);
  349. const scores = data.score;
  350. console.log("Move scores:", scores);
  351. // Display the evaluations on the board
  352. displayEvaluations(scores);
  353. // Choose the best move based on the selected strategy
  354. const bestMove = chooseBestMove(scores);
  355. console.log(`Best move (column): ${bestMove + 1} with strategy: ${bestMoveStrategy}`);
  356. if (bestMove !== -1) {
  357. clickColumn(bestMove + 1); // Convert from 0-indexed to 1-indexed
  358. } else {
  359. console.log("No valid moves available");
  360. processingMove = false;
  361. }
  362. } catch (error) {
  363. console.error("Error parsing API response:", error);
  364. processingMove = false;
  365. }
  366. },
  367. onerror: function(error) {
  368. console.error("API request failed:", error);
  369. processingMove = false;
  370. }
  371. });
  372. }
  373.  
  374. // Make a move using the human mode API (kevinalbs.com)
  375. function makeHumanModeAPIMove() {
  376. const boardState = getHumanModeBoardState();
  377. console.log("Current board state (human mode):", boardState);
  378.  
  379. // Convert player from R/Y to 1/2
  380. let humanModePlayer;
  381. if (player === 'R') {
  382. humanModePlayer = '1';
  383. } else if (player === 'Y') {
  384. humanModePlayer = '2';
  385. } else {
  386. // If player is not set, try to determine it from the board state
  387. console.log("Player not set, attempting to determine from board state");
  388. // Count pieces to determine whose turn it is
  389. let count1 = 0;
  390. let count2 = 0;
  391. for (let i = 0; i < boardState.length; i++) {
  392. if (boardState[i] === '1') count1++;
  393. if (boardState[i] === '2') count2++;
  394. }
  395. // If equal counts or more 1s, it's player 2's turn, otherwise player 1's turn
  396. humanModePlayer = count1 <= count2 ? '1' : '2';
  397. console.log(`Determined player: ${humanModePlayer} (counts: 1=${count1}, 2=${count2})`);
  398. }
  399. const apiUrl = `https://kevinalbs.com/connect4/back-end/index.php/getMoves?board_data=${boardState}&player=${humanModePlayer}`;
  400. console.log("Human Mode API URL:", apiUrl);
  401.  
  402. GM.xmlHttpRequest({
  403. method: "GET",
  404. url: apiUrl,
  405. onload: function(response) {
  406. console.log("Human Mode API response received:", response.responseText);
  407. try {
  408. const data = JSON.parse(response.responseText);
  409. console.log("Parsed Human Mode API data:", data);
  410.  
  411. let bestMove = -1;
  412. let bestScore = -Infinity;
  413. // Find the best move based on the scores
  414. for (let move in data) {
  415. if (data[move] > bestScore) {
  416. bestScore = data[move];
  417. bestMove = parseInt(move);
  418. }
  419. }
  420.  
  421. console.log("Best move (column):", bestMove);
  422. if (bestMove !== -1) {
  423. // Display evaluations in a format compatible with the display function
  424. const scores = Array(7).fill(100); // Default to invalid
  425. for (let move in data) {
  426. scores[parseInt(move)] = data[move];
  427. }
  428. displayEvaluations(scores);
  429. clickColumn(bestMove + 1); // Convert from 0-indexed to 1-indexed
  430. } else {
  431. console.log("No valid moves available");
  432. processingMove = false;
  433. }
  434. } catch (error) {
  435. console.error("Error parsing Human Mode API response:", error);
  436. processingMove = false;
  437. }
  438. },
  439. onerror: function(error) {
  440. console.error("Human Mode API request failed:", error);
  441. processingMove = false;
  442. }
  443. });
  444. }
  445.  
  446. // Choose the best move based on the selected strategy
  447. function chooseBestMove(scores) {
  448. // Filter out invalid moves (score = 100 means column is full)
  449. const validMoves = scores.map((score, index) => ({ score, index }))
  450. .filter(move => move.score !== 100);
  451. if (validMoves.length === 0) return -1;
  452. switch (bestMoveStrategy) {
  453. case 'optimal':
  454. // Choose the move with the highest score
  455. return validMoves.reduce((best, current) =>
  456. current.score > best.score ? current : best, validMoves[0]).index;
  457. case 'random':
  458. // Choose a random valid move
  459. return validMoves[Math.floor(Math.random() * validMoves.length)].index;
  460. case 'defensive':
  461. // Choose the move that minimizes opponent's advantage
  462. // For negative scores, choose the least negative
  463. // For positive scores, choose the highest
  464. return validMoves.reduce((best, current) => {
  465. if (best.score < 0 && current.score < 0) {
  466. return current.score > best.score ? current : best;
  467. } else {
  468. return current.score > best.score ? current : best;
  469. }
  470. }, validMoves[0]).index;
  471. default:
  472. // Default to optimal
  473. return validMoves.reduce((best, current) =>
  474. current.score > best.score ? current : best, validMoves[0]).index;
  475. }
  476. }
  477.  
  478. // Display evaluations on the board
  479. function displayEvaluations(scores) {
  480. const boardContainer = document.querySelector(".grid.size6x7");
  481. let evalContainer = document.querySelector("#evaluation-container");
  482.  
  483. if (!evalContainer) {
  484. evalContainer = document.createElement("div");
  485. evalContainer.id = "evaluation-container";
  486. evalContainer.style.display = "flex";
  487. evalContainer.style.justifyContent = "space-around";
  488. evalContainer.style.marginTop = "10px";
  489. evalContainer.style.fontFamily = "Arial, sans-serif";
  490. boardContainer.parentNode.insertBefore(evalContainer, boardContainer.nextSibling);
  491. }
  492.  
  493. // Clear existing evaluation cells
  494. evalContainer.innerHTML = '';
  495.  
  496. scores.forEach((score, index) => {
  497. const evalCell = document.createElement("div");
  498. evalCell.textContent = score === 100 ? "X" : score; // Show X for invalid moves
  499. evalCell.style.textAlign = 'center';
  500. evalCell.style.fontWeight = 'bold';
  501. evalCell.style.fontSize = '16px';
  502. evalCell.style.width = '40px';
  503. evalCell.style.padding = '5px';
  504. evalCell.style.borderRadius = '5px';
  505. // Color based on score
  506. if (score === 100) {
  507. evalCell.style.color = '#888'; // Gray for invalid moves
  508. } else if (score > 0) {
  509. evalCell.style.backgroundColor = `rgba(0, 128, 0, ${Math.min(Math.abs(score) / 20, 1)})`;
  510. evalCell.style.color = 'white';
  511. } else if (score < 0) {
  512. evalCell.style.backgroundColor = `rgba(255, 0, 0, ${Math.min(Math.abs(score) / 20, 1)})`;
  513. evalCell.style.color = 'white';
  514. } else {
  515. evalCell.style.color = 'black';
  516. }
  517. evalContainer.appendChild(evalCell);
  518. });
  519. }
  520.  
  521. // Initialize AI player information
  522. function initAITurn() {
  523. const boardState = getBoardState();
  524. if (!player) {
  525. for (let row of boardState) {
  526. for (let cell of row) {
  527. if (cell !== "E") {
  528. player = cell === "R" ? "Y" : "R";
  529. break;
  530. }
  531. }
  532. if (player) break;
  533. }
  534. }
  535. }
  536.  
  537. // Logout function
  538. function logout() {
  539. GM.setValue('username', '');
  540. location.reload();
  541. }
  542.  
  543. // Update server status indicator
  544. function updateServerStatusIndicator(isAvailable, isCalibrated) {
  545. const $status = $('#python-server-status');
  546. if (isAvailable) {
  547. if (isCalibrated) {
  548. $status.text('Python Server: Connected & Calibrated')
  549. .css('backgroundColor', '#28a745');
  550. } else {
  551. $status.text('Python Server: Connected (Not Calibrated)')
  552. .css('backgroundColor', '#ffc107');
  553. }
  554. } else {
  555. $status.text('Python Server: Disconnected')
  556. .css('backgroundColor', '#dc3545');
  557. }
  558. // Disable auto-play if server is not available or not calibrated
  559. if ((!isAvailable || (isAvailable && !isCalibrated)) && autoPlayEnabled) {
  560. autoPlayEnabled = false;
  561. const $btn = $('#auto-play-toggle');
  562. $btn.text('Auto-Play: OFF')
  563. .removeClass('btn-success')
  564. .addClass('btn-danger');
  565. if (!isAvailable) {
  566. console.log("Auto-play disabled because Python server is not available");
  567. } else if (!isCalibrated) {
  568. console.log("Auto-play disabled because Python server is not calibrated");
  569. alert("Please calibrate the board in the Python application before enabling Auto-Play.");
  570. }
  571. }
  572. }
  573. // Toggle auto-play functionality
  574. function toggleAutoPlay() {
  575. // Don't allow enabling auto-play if Python server is not available
  576. if (!pythonServerAvailable && !autoPlayEnabled) {
  577. alert("Cannot enable Auto-Play: Python server is not connected.");
  578. return;
  579. }
  580. // Check if the server is calibrated
  581. GM.xmlHttpRequest({
  582. method: "GET",
  583. url: `${PYTHON_SERVER_URL}/api/status`,
  584. onload: function(response) {
  585. try {
  586. const data = JSON.parse(response.responseText);
  587. if (!data.calibrated && !autoPlayEnabled) {
  588. alert("Cannot enable Auto-Play: Board is not calibrated. Please calibrate the board in the Python application first.");
  589. return;
  590. }
  591. // If we get here, we can toggle auto-play
  592. autoPlayEnabled = !autoPlayEnabled;
  593. const $btn = $('#auto-play-toggle');
  594. if (autoPlayEnabled) {
  595. $btn.text('Auto-Play: ON')
  596. .removeClass('btn-danger')
  597. .addClass('btn-success');
  598. } else {
  599. $btn.text('Auto-Play: OFF')
  600. .removeClass('btn-success')
  601. .addClass('btn-danger');
  602. }
  603. } catch (error) {
  604. console.error('Error checking calibration status:', error);
  605. alert("Cannot enable Auto-Play: Error checking calibration status.");
  606. }
  607. },
  608. onerror: function(error) {
  609. console.error('Error checking server status:', error);
  610. alert("Cannot enable Auto-Play: Python server is not connected.");
  611. }
  612. });
  613. }
  614.  
  615. // Toggle API selection
  616. async function toggleAPI() {
  617. selectedAPI = selectedAPI === 'gamesolver' ? 'human' : 'gamesolver';
  618. await GM.setValue('selectedAPI', selectedAPI);
  619. const $btn = $('#api-toggle');
  620. if (selectedAPI === 'gamesolver') {
  621. $btn.text('API: GameSolver')
  622. .removeClass('btn-info')
  623. .addClass('btn-primary');
  624. console.log("Switched to GameSolver API");
  625. } else {
  626. $btn.text('API: Human Mode')
  627. .removeClass('btn-primary')
  628. .addClass('btn-info');
  629. console.log("Switched to Human Mode API");
  630. }
  631. }
  632.  
  633. // Get the current state of the board for the human mode API
  634. function getHumanModeBoardState() {
  635. const boardContainer = document.querySelector(".grid.size6x7");
  636. if (!boardContainer) {
  637. console.error("Board container not found");
  638. return "";
  639. }
  640. // The Human Mode API expects a 42-character string representing the board
  641. // from top to bottom, left to right (0 = empty, 1 = dark/red, 2 = light/yellow)
  642. let boardState = "";
  643. // Iterate over cells in a more flexible way
  644. for (let row = 1; row <= 6; row++) {
  645. for (let col = 1; col <= 7; col++) {
  646. // Use a selector that matches the class names correctly
  647. const cellSelector = `.grid-item.cell-${row}-${col}`;
  648. const cell = boardContainer.querySelector(cellSelector);
  649. if (cell) {
  650. // Check the circle class names to determine the cell's state
  651. const circle = cell.querySelector("circle");
  652. if (circle) {
  653. if (circle.classList.contains("circle-dark")) {
  654. boardState += "1";
  655. } else if (circle.classList.contains("circle-light")) {
  656. boardState += "2";
  657. } else {
  658. boardState += "0";
  659. }
  660. } else {
  661. boardState += "0";
  662. }
  663. } else {
  664. console.error(`Cell not found: ${cellSelector}`);
  665. boardState += "0";
  666. }
  667. }
  668. }
  669. console.log("Human Mode board state (42-char string):", boardState);
  670. return boardState;
  671. }
  672.  
  673. // Create the UI elements with a calibration button
  674. function createUI() {
  675. // Create main container
  676. const $container = $('<div>')
  677. .attr('id', 'connect4-ai-controls')
  678. .css({
  679. position: 'fixed',
  680. bottom: '20px',
  681. right: '20px',
  682. zIndex: '9999',
  683. display: 'flex',
  684. flexDirection: 'column',
  685. gap: '10px',
  686. alignItems: 'flex-end'
  687. })
  688. .appendTo('body');
  689. // Create server status indicator
  690. const $serverStatus = $('<div>')
  691. .attr('id', 'python-server-status')
  692. .text('Python Server: Checking...')
  693. .css({
  694. padding: '5px 10px',
  695. backgroundColor: '#333',
  696. color: 'white',
  697. borderRadius: '5px',
  698. fontSize: '12px',
  699. marginBottom: '5px'
  700. })
  701. .appendTo($container);
  702. // Create calibration button
  703. const $calibrateBtn = $('<button>')
  704. .text('Calibrate Board')
  705. .addClass('btn btn-warning')
  706. .css({
  707. padding: '5px 10px',
  708. borderRadius: '5px',
  709. cursor: 'pointer',
  710. fontWeight: 'bold',
  711. border: 'none',
  712. marginBottom: '5px'
  713. })
  714. .on('click', function() {
  715. if (!pythonServerAvailable) {
  716. alert("Python server is not connected. Cannot calibrate.");
  717. return;
  718. }
  719. alert("Please switch to the Python application window and follow the calibration instructions.");
  720. // Send a message to the Python server to start calibration
  721. GM.xmlHttpRequest({
  722. method: "POST",
  723. url: `${PYTHON_SERVER_URL}/api/calibrate`,
  724. headers: {
  725. "Content-Type": "application/json"
  726. },
  727. data: JSON.stringify({ start: true }),
  728. onload: function(response) {
  729. console.log('Calibration request sent');
  730. },
  731. onerror: function(error) {
  732. console.error('Error sending calibration request:', error);
  733. }
  734. });
  735. })
  736. .appendTo($container);
  737. // Create auto-play toggle button
  738. const $autoPlayBtn = $('<button>')
  739. .attr('id', 'auto-play-toggle')
  740. .text('Auto-Play: ON')
  741. .addClass('btn btn-success')
  742. .css({
  743. padding: '5px 10px',
  744. borderRadius: '5px',
  745. cursor: 'pointer',
  746. fontWeight: 'bold',
  747. border: 'none'
  748. })
  749. .on('click', toggleAutoPlay)
  750. .appendTo($container);
  751. // Create auto-queue toggle button (moved up in the UI for better visibility)
  752. const $autoQueueBtn = $('<button>')
  753. .attr('id', 'auto-queue-toggle')
  754. .text('Auto-Queue: OFF')
  755. .addClass('btn btn-danger')
  756. .attr('title', 'Automatically leaves room and queues for a new game when a game ends')
  757. .css({
  758. padding: '5px 10px',
  759. borderRadius: '5px',
  760. cursor: 'pointer',
  761. fontWeight: 'bold',
  762. border: 'none',
  763. marginTop: '5px'
  764. })
  765. .on('click', toggleAutoQueue)
  766. .appendTo($container);
  767. // Create keyboard controls toggle button
  768. const $keyboardBtn = $('<button>')
  769. .attr('id', 'keyboard-controls-toggle')
  770. .text('Keyboard Controls: ON')
  771. .addClass('btn btn-success')
  772. .css({
  773. padding: '5px 10px',
  774. borderRadius: '5px',
  775. cursor: 'pointer',
  776. fontWeight: 'bold',
  777. border: 'none',
  778. marginTop: '5px'
  779. })
  780. .on('click', toggleKeyboardControls)
  781. .appendTo($container);
  782. // Create API toggle button
  783. const $apiToggleBtn = $('<button>')
  784. .attr('id', 'api-toggle')
  785. .text(selectedAPI === 'gamesolver' ? 'API: GameSolver' : 'API: Human Mode')
  786. .addClass(selectedAPI === 'gamesolver' ? 'btn btn-primary' : 'btn btn-info')
  787. .css({
  788. padding: '5px 10px',
  789. borderRadius: '5px',
  790. cursor: 'pointer',
  791. fontWeight: 'bold',
  792. border: 'none',
  793. marginTop: '5px'
  794. })
  795. .on('click', toggleAPI)
  796. .appendTo($container);
  797. // Create keyboard shortcuts info
  798. const $keyboardInfo = $('<div>')
  799. .css({
  800. backgroundColor: '#333',
  801. color: 'white',
  802. padding: '8px',
  803. borderRadius: '5px',
  804. fontSize: '12px',
  805. marginTop: '5px',
  806. maxWidth: '200px'
  807. })
  808. .html('Keyboard Shortcuts:<br>1-7: Click column<br>A: Toggle Auto-Play<br>K: Toggle Keyboard<br>H: Toggle API<br>Q: Toggle Auto-Queue')
  809. .appendTo($container);
  810. // Create strategy selector
  811. const $strategyContainer = $('<div>')
  812. .css({
  813. display: 'flex',
  814. flexDirection: 'column',
  815. backgroundColor: '#333',
  816. padding: '10px',
  817. borderRadius: '5px',
  818. marginBottom: '5px'
  819. })
  820. .appendTo($container);
  821. $('<div>')
  822. .text('AI Strategy:')
  823. .css({
  824. color: 'white',
  825. marginBottom: '5px',
  826. fontSize: '12px'
  827. })
  828. .appendTo($strategyContainer);
  829. const $strategySelect = $('<select>')
  830. .attr('id', 'strategy-select')
  831. .css({
  832. padding: '5px',
  833. borderRadius: '3px',
  834. border: 'none'
  835. })
  836. .on('change', function() {
  837. bestMoveStrategy = $(this).val();
  838. console.log(`Strategy changed to: ${bestMoveStrategy}`);
  839. })
  840. .appendTo($strategyContainer);
  841. $('<option>').val('optimal').text('Optimal').appendTo($strategySelect);
  842. $('<option>').val('defensive').text('Defensive').appendTo($strategySelect);
  843. $('<option>').val('random').text('Random').appendTo($strategySelect);
  844. // Create manual column click buttons
  845. const $columnButtonsContainer = $('<div>')
  846. .css({
  847. display: 'flex',
  848. flexDirection: 'column',
  849. backgroundColor: '#333',
  850. padding: '10px',
  851. borderRadius: '5px',
  852. marginBottom: '5px'
  853. })
  854. .appendTo($container);
  855. $('<div>')
  856. .text('Manual Column Click:')
  857. .css({
  858. color: 'white',
  859. marginBottom: '5px',
  860. fontSize: '12px'
  861. })
  862. .appendTo($columnButtonsContainer);
  863. const $buttonRow = $('<div>')
  864. .css({
  865. display: 'flex',
  866. gap: '3px'
  867. })
  868. .appendTo($columnButtonsContainer);
  869. // Add column buttons
  870. for (let i = 1; i <= 7; i++) {
  871. $('<button>')
  872. .text(i)
  873. .css({
  874. width: '25px',
  875. height: '25px',
  876. padding: '0',
  877. fontSize: '12px',
  878. textAlign: 'center',
  879. borderRadius: '3px',
  880. cursor: 'pointer',
  881. backgroundColor: '#007bff',
  882. color: 'white',
  883. border: 'none'
  884. })
  885. .on('click', function() {
  886. if (!processingMove && pythonServerAvailable) {
  887. processingMove = true;
  888. clickColumn(i);
  889. }
  890. })
  891. .appendTo($buttonRow);
  892. }
  893. // Create logout button
  894. const $logoutBtn = $('<button>')
  895. .text('Logout')
  896. .addClass('btn btn-secondary')
  897. .css({
  898. padding: '5px 10px',
  899. borderRadius: '5px',
  900. cursor: 'pointer',
  901. marginTop: '5px'
  902. })
  903. .on('click', logout)
  904. .appendTo($container);
  905. // Initialize auto-queue state
  906. updateAutoQueueButton();
  907. }
  908. // Auto-queue functionality
  909. async function toggleAutoQueue() {
  910. isAutoQueueOn = !isAutoQueueOn;
  911. await GM.setValue('autoQueueEnabled', isAutoQueueOn);
  912. updateAutoQueueButton();
  913. console.log(`Auto-Queue ${isAutoQueueOn ? 'enabled' : 'disabled'}`);
  914. if (isAutoQueueOn) {
  915. showAutoQueueNotification("Auto-Queue enabled - will automatically join new games");
  916. } else {
  917. showAutoQueueNotification("Auto-Queue disabled");
  918. }
  919. }
  920. function updateAutoQueueButton() {
  921. const $btn = $('#auto-queue-toggle');
  922. if (isAutoQueueOn) {
  923. $btn.text('Auto-Queue: ON')
  924. .removeClass('btn-danger')
  925. .addClass('btn-success');
  926. } else {
  927. $btn.text('Auto-Queue: OFF')
  928. .removeClass('btn-success')
  929. .addClass('btn-danger');
  930. }
  931. }
  932.  
  933. function clickLeaveRoomButton() {
  934. const leaveButton = $("button.btn-light.ng-tns-c189-7");
  935. if (leaveButton.length) {
  936. console.log("Auto-Queue: Clicking leave room button");
  937. leaveButton.click();
  938. return true;
  939. }
  940. return false;
  941. }
  942.  
  943. function clickPlayOnlineButton() {
  944. const playButton = document.querySelector("body > app-root > app-navigation > div.d-flex.h-100 > div.d-flex.flex-column.h-100.w-100 > main > app-game-landing > div > div > div > div.col-12.col-lg-9.dashboard > div.card.area-buttons.d-flex.justify-content-center.align-items-center.flex-column > button.btn.btn-secondary.btn-lg.position-relative");
  945. if (playButton) {
  946. console.log("Auto-Queue: Clicking play online button");
  947. playButton.click();
  948. return true;
  949. }
  950. return false;
  951. }
  952.  
  953. function checkButtonsPeriodically() {
  954. if (!isAutoQueueOn) return;
  955. // Try to leave room first
  956. if (clickLeaveRoomButton()) {
  957. // Add visual feedback
  958. showAutoQueueNotification("Auto-Queue: Leaving room...");
  959. return;
  960. }
  961. // If we couldn't leave (maybe already left), try to play online
  962. if (clickPlayOnlineButton()) {
  963. showAutoQueueNotification("Auto-Queue: Joining new game...");
  964. return;
  965. }
  966. // Check for other buttons that might indicate game end
  967. const playAgainButton = $("button:contains('Play Again')");
  968. if (playAgainButton.length) {
  969. console.log("Auto-Queue: Clicking play again button");
  970. playAgainButton.click();
  971. showAutoQueueNotification("Auto-Queue: Playing again...");
  972. return;
  973. }
  974. }
  975.  
  976. // Show a temporary notification for auto-queue actions
  977. function showAutoQueueNotification(message) {
  978. let $notification = $('#auto-queue-notification');
  979. if (!$notification.length) {
  980. $notification = $('<div>')
  981. .attr('id', 'auto-queue-notification')
  982. .css({
  983. position: 'fixed',
  984. top: '20px',
  985. right: '20px',
  986. backgroundColor: 'rgba(0, 0, 0, 0.7)',
  987. color: 'white',
  988. padding: '10px 15px',
  989. borderRadius: '5px',
  990. zIndex: '10000',
  991. fontSize: '14px',
  992. fontWeight: 'bold',
  993. opacity: '0',
  994. transition: 'opacity 0.3s ease'
  995. })
  996. .appendTo('body');
  997. }
  998. $notification.text(message)
  999. .css('opacity', '1');
  1000. // Hide after 3 seconds
  1001. setTimeout(() => {
  1002. $notification.css('opacity', '0');
  1003. }, 3000);
  1004. }
  1005.  
  1006. // Handle countdown clicks for auto-queue
  1007. let previousNumber = null;
  1008. function trackAndClickIfDifferent() {
  1009. const $spanElement = $('app-count-down span');
  1010. if ($spanElement.length) {
  1011. const number = parseInt($spanElement.text(), 10);
  1012. if (!isNaN(number) && previousNumber !== null && number !== previousNumber && isAutoQueueOn) {
  1013. $spanElement.click();
  1014. }
  1015. previousNumber = number;
  1016. }
  1017. }
  1018.  
  1019. // Display board state in console for debugging
  1020. function displayAIBoard() {
  1021. const boardState = getBoardState();
  1022. console.log("Current board state:");
  1023. boardState.forEach(row => {
  1024. console.log(row.join(" | "));
  1025. });
  1026. }
  1027.  
  1028. // Initialize the script
  1029. async function initialize() {
  1030. console.log("Connect 4 AI script initializing...");
  1031. // Load auto-queue state from storage
  1032. isAutoQueueOn = await GM.getValue('autoQueueEnabled', AUTO_QUEUE_ENABLED_DEFAULT);
  1033. console.log(`Auto-Queue initialized: ${isAutoQueueOn ? 'ON' : 'OFF'}`);
  1034. // Create UI elements
  1035. createUI();
  1036. // Check Python server status initially and periodically
  1037. checkPythonServerStatus();
  1038. setInterval(checkPythonServerStatus, SERVER_CHECK_INTERVAL);
  1039. // Set up game state monitoring
  1040. setInterval(function() {
  1041. updateBoard();
  1042. initAITurn();
  1043. }, BOARD_CHECK_INTERVAL);
  1044. // Set up reset button monitoring
  1045. setInterval(checkForResetButtons, RESET_CHECK_INTERVAL);
  1046. // Set up auto-queue functionality
  1047. setInterval(checkButtonsPeriodically, AUTO_QUEUE_CHECK_INTERVAL);
  1048. setInterval(trackAndClickIfDifferent, AUTO_QUEUE_CHECK_INTERVAL);
  1049. // Set up move detection
  1050. setInterval(detectNewMove, 100);
  1051. // Debug board display
  1052. if (localStorage.getItem('debugMode') === 'true') {
  1053. setInterval(displayAIBoard, 5000);
  1054. }
  1055. // Set up keyboard controls
  1056. setupKeyboardControls();
  1057. console.log("Connect 4 AI script loaded and running");
  1058. }
  1059. // Start the script
  1060. initialize();
  1061. })();