// ==UserScript==
// @name Tic Tac Toe AI for papergames
// @namespace https://github.com/longkidkoolstar
// @version 3.0
// @description AI plays Tic-Tac-Toe for you on papergames.io. Have fun and destroy some nerds 😃!!
// @author longkidkoolstar
// @icon https://th.bing.com/th/id/R.3502d1ca849b062acb85cf68a8c48bcd?rik=LxTvt1UpLC2y2g&pid=ImgRaw&r=0
// @match https://papergames.io/*
// @license none
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.deleteValue
// ==/UserScript==
(function() {
'use strict';
var depth;
// Function to check if the element is visible
function isElementVisible(element) {
return element && element.style.display !== 'none';
}
// Function to check for the element and click it when it becomes visible
function waitForElementAndClick(targetElementSelector, triggerElementSelector, pollingInterval) {
var xMark = document.querySelector(targetElementSelector);
var countDown = document.querySelector(triggerElementSelector);
var intervalId = setInterval(function() {
// Check if the countDown element is now visible
if (isElementVisible(countDown)) {
console.log("Element is visible. Clicking.");
xMark.click();
clearInterval(intervalId); // Stop polling
}
}, pollingInterval);
}
// Start polling every 1 second (adjust the interval as needed)
waitForElementAndClick('svg.fa-xmark', 'app-count-down span', 1000);
function getBoardState() {
var boardState = [];
var gridItems = document.querySelectorAll('.grid.s-3x3 .grid-item');
for (var i = 0; i < 3; i++) {
var row = [];
for (var j = 0; j < 3; j++) {
var cell = gridItems[i * 3 + j];
var svg = cell.querySelector('svg');
if (svg) {
var label = svg.getAttribute('aria-label');
if (label.toLowerCase().includes('x')) {
row.push('x');
} else if (label.toLowerCase().includes('o') || label.toLowerCase().includes('circle')) {
row.push('o');
} else {
row.push('_');
}
} else {
row.push('_'); // An empty cell
}
}
boardState.push(row);
}
return boardState;
}
function simulateCellClick(row, col) {
var gridItems = document.querySelectorAll('.grid.s-3x3 .grid-item');
var cell = gridItems[row * 3 + col];
if (cell) {
var event = new MouseEvent('click', {
bubbles: true,
cancelable: true,
//view: window
});
cell.dispatchEvent(event);
}
}
var prevChronometerValue = null;
// Check if username is stored in GM storage
GM.getValue('username').then(function(username) {
if (!username) {
// Alert the user
alert('Username is not stored in GM storage.');
// Prompt the user to enter the username
username = prompt('Please enter your Papergames username (case-sensitive):');
// Save the username to GM storage
GM.setValue('username', username);
}
});
function logout() {
GM.deleteValue('username');
location.reload();
}
function createLogoutButton() {
var logoutButton = document.createElement('button');
logoutButton.textContent = 'Logout';
logoutButton.style.position = 'fixed';
logoutButton.style.bottom = '20px';
logoutButton.style.right = '20px';
logoutButton.style.zIndex = '9999';
logoutButton.style.color = 'white'; // Set the text color to white
logoutButton.classList.add('btn', 'btn-secondary', 'mb-2', 'ng-star-inserted');
logoutButton.addEventListener('click', logout);
logoutButton.addEventListener('mouseover', function() {
logoutButton.style.opacity = '0.5'; // Dim the button when hovered over
});
logoutButton.addEventListener('mouseout', function() {
logoutButton.style.opacity = '1'; // Restore the button opacity when mouse leaves
});
document.body.appendChild(logoutButton);
}
createLogoutButton();
//------------------------------------------------
(function() {
'use strict';
// Create a container for the dropdown
var dropdownContainer = document.createElement('div');
dropdownContainer.style.position = 'fixed';
dropdownContainer.style.bottom = '20px';
dropdownContainer.style.left = '20px';
dropdownContainer.style.zIndex = '9998';
dropdownContainer.style.backgroundColor = '#1b2837';
dropdownContainer.style.border = '1px solid #18bc9c';
dropdownContainer.style.borderRadius = '5px';
// Create a button to toggle the dropdown
var toggleButton = document.createElement('button');
toggleButton.textContent = 'Settings';
toggleButton.style.padding = '5px 10px';
toggleButton.style.border = 'none';
toggleButton.classList.add('btn', 'btn-secondary', 'mb-2', 'ng-star-inserted');
toggleButton.style.backgroundColor = '#007bff';
toggleButton.style.color = 'white';
toggleButton.style.borderRadius = '5px';
toggleButton.addEventListener('mouseover', function() {
toggleButton.style.opacity = '0.5'; // Dim the button when hovered over
});
toggleButton.addEventListener('mouseout', function() {
toggleButton.style.opacity = '1'; // Restore the button opacity when mouse leaves
});
// Create the dropdown content
var dropdownContent = document.createElement('div');
dropdownContent.style.display = 'none';
dropdownContent.style.padding = '8px';
// Create the "Auto Queue" tab
var autoQueueTab = document.createElement('div');
autoQueueTab.textContent = 'Auto Queue';
autoQueueTab.style.padding = '5px 0';
autoQueueTab.style.cursor = 'pointer';
// Create the "Depth Slider" tab
var depthSliderTab = document.createElement('div');
depthSliderTab.textContent = 'Depth Slider';
depthSliderTab.style.padding = '5px 0';
depthSliderTab.style.cursor = 'pointer';
// Create the settings for "Auto Queue"
var autoQueueSettings = document.createElement('div');
autoQueueSettings.textContent = 'Auto Queue Settings';
autoQueueSettings.style.display = 'none'; // Initially hidden
autoQueueSettings.style.padding = '10px';
// Create the settings for "Depth Slider"
var depthSliderSettings = document.createElement('div');
depthSliderSettings.style.display = 'none'; // Initially displayed for this tab
depthSliderSettings.style.padding = '10px';
// Create the depth slider
var depthSlider = document.createElement('input');
depthSlider.type = 'range';
depthSlider.min = '1';
depthSlider.max = '100';
GM.getValue('depth').then(function(storedDepth) {
depthSlider.value = storedDepth !== null ? storedDepth : '100';
});
// Add event listener to the depth slider
depthSlider.addEventListener('input', function(event) {
var depth = Math.round(depthSlider.value);
GM.setValue('depth', depth.toString());
// Show the popup with the current depth value
var popup = document.querySelector('.depth-popup'); // Use an existing popup or create a new one
if (!popup) {
popup = document.createElement('div');
popup.classList.add('depth-popup');
popup.style.position = 'fixed';
popup.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
popup.style.color = 'white';
popup.style.padding = '5px 10px';
popup.style.borderRadius = '5px';
popup.style.zIndex = '9999';
popup.style.display = 'none';
document.body.appendChild(popup);
}
popup.innerText = 'Depth: ' + depth;
popup.style.display = 'block';
// Calculate slider position and adjust popup position
var sliderRect = depthSlider.getBoundingClientRect();
var popupX = sliderRect.left + ((depthSlider.value - depthSlider.min) / (depthSlider.max - depthSlider.min)) * sliderRect.width - popup.clientWidth / 2;
var popupY = sliderRect.top - popup.clientHeight - 10;
popup.style.left = popupX + 'px';
popup.style.top = popupY + 'px';
// Start a timer to hide the popup after a certain duration (e.g., 2 seconds)
setTimeout(function() {
popup.style.display = 'none';
}, 2000);
});
// Append the depth slider to the "Depth Slider" settings
depthSliderSettings.appendChild(depthSlider);
// Create the settings for "Auto Queue"
var autoQueueSettings = document.createElement('div');
autoQueueSettings.style.padding = '10px';
// Create the "Auto Queue" toggle button
var autoQueueToggleButton = document.createElement('button');
autoQueueToggleButton.textContent = 'Auto Queue Off';
autoQueueToggleButton.style.marginTop = '10px';
autoQueueToggleButton.style.display = 'none';
autoQueueToggleButton.classList.add('btn', 'btn-secondary', 'mb-2', 'ng-star-inserted');
autoQueueToggleButton.style.backgroundColor = 'red'; // Initially red for "Off"
autoQueueToggleButton.style.color = 'white';
autoQueueToggleButton.addEventListener('click', toggleAutoQueue);
autoQueueSettings.appendChild(autoQueueToggleButton);
var isAutoQueueOn = false; // Track the state
function toggleAutoQueue() {
// Toggle the state
isAutoQueueOn = !isAutoQueueOn;
GM.setValue('isToggled', isAutoQueueOn);
// Update the button text and style based on the state
autoQueueToggleButton.textContent = isAutoQueueOn ? 'Auto Queue On' : 'Auto Queue Off';
autoQueueToggleButton.style.backgroundColor = isAutoQueueOn ? 'green' : 'red';
}
function clickLeaveRoomButton() {
var leaveRoomButton = document.querySelector("button.btn-light.ng-tns-c189-7");
if (leaveRoomButton) {
leaveRoomButton.click();
}
}
function clickPlayOnlineButton() {
var playOnlineButton = document.querySelector("button.btn-secondary.flex-grow-1");
if (playOnlineButton) {
playOnlineButton.click();
}
}
// Periodically check for buttons when the toggle is on
function checkButtonsPeriodically() {
if (isAutoQueueOn) {
clickLeaveRoomButton();
clickPlayOnlineButton();
}
}
// Set up periodic checking
setInterval(checkButtonsPeriodically, 1000);
//------------------------------------------------------------------------Testing Purposes
let previousNumber = null; // Initialize the previousNumber to null
function trackAndClickIfDifferent() {
// Select the <span> element using its class name
const spanElement = document.querySelector('app-count-down span');
if (spanElement) {
// Extract the number from the text content
const number = parseInt(spanElement.textContent, 10);
// Check if parsing was successful
if (!isNaN(number)) {
// Check if the number has changed since the last check
if (previousNumber !== null && number !== previousNumber && isAutoQueueOn) {
spanElement.click();
}
// Update the previousNumber with the current value
previousNumber = number;
}
}
}
// Set up an interval to call the function at regular intervals (e.g., every 1 second)
setInterval(trackAndClickIfDifferent, 1000); // 1000 milliseconds = 1 second
//-------------------------------------------------------------------------------------------
// Append the toggle button to the "Auto Queue" settings
autoQueueSettings.appendChild(autoQueueToggleButton);
// Add event listeners to the tabs to toggle their respective settings
autoQueueTab.addEventListener('click', function() {
// Hide the depth slider settings
depthSliderSettings.style.display = 'none';
// Show the auto queue settings
autoQueueSettings.style.display = 'block';
autoQueueToggleButton.style.display = 'block';
});
depthSliderTab.addEventListener('click', function() {
// Hide the auto queue settings
autoQueueSettings.style.display = 'none';
// Show the depth slider settings
depthSliderSettings.style.display = 'block';
});
// Append the tabs and settings to the dropdown content
dropdownContent.appendChild(autoQueueTab);
dropdownContent.appendChild(autoQueueSettings);
dropdownContent.appendChild(depthSliderTab);
dropdownContent.appendChild(depthSliderSettings);
// Append the button and dropdown content to the container
dropdownContainer.appendChild(toggleButton);
dropdownContainer.appendChild(dropdownContent);
// Toggle the dropdown when the button is clicked
toggleButton.addEventListener('click', function() {
if (dropdownContent.style.display === 'none') {
dropdownContent.style.display = 'block';
} else {
dropdownContent.style.display = 'none';
}
});
// Append the dropdown container to the document body
document.body.appendChild(dropdownContainer);
})();
//------------------------------------------------
function updateBoard(squareId) {
var row = parseInt(squareId[0]);
var col = parseInt(squareId[1]);
GM.getValue("username").then(function(username) {
var profileOpeners = document.querySelectorAll(".text-truncate.cursor-pointer");
var profileOpener = null;
profileOpeners.forEach(function(opener) {
if (opener.textContent.trim() === username) {
profileOpener = opener;
}
});
if (!profileOpener) {
console.error("Profile opener not found");
return;
}
var chronometer = document.querySelector("app-chronometer");
var numberElement = profileOpener.parentNode ? profileOpener.parentNode.querySelectorAll("span")[4] : null;
var profileOpenerParent = profileOpener.parentNode ? profileOpener.parentNode.parentNode : null;
var svgElement = profileOpenerParent.querySelector("circle[class*='circle-dark-stroked']");
if (!svgElement) {
svgElement = profileOpenerParent.querySelector("svg[class*='fa-xmark']");
}
if (svgElement && svgElement.closest("circle[class*='circle-dark-stroked']")) {
player = 'o'; // Player is playing as "O"
} else if (svgElement && svgElement.closest("svg[class*='fa-xmark']")) {
player = 'x'; // Player is playing as "X"
}
var currentElement = chronometer || numberElement;
if (currentElement.textContent !== prevChronometerValue && profileOpener) {
prevChronometerValue = currentElement.textContent;
simulateCellClick(row, col);
} else {
console.log("Waiting for AI's turn...");
}
});
return player;
}
function logBoardState() {
// Attempt to log various variables and elements for debugging
try {
// Log row and col based on a hardcoded squareId for debugging
var squareId = "00"; // Change this as needed for different squares
var row = parseInt(squareId[0]);
var col = parseInt(squareId[1]);
console.log("Row:", row, "Col:", col);
// Log username from GM storage
GM.getValue("username").then(function(username) {
console.log("Username from GM storage:", username);
// Log profile openers
var profileOpeners = document.querySelectorAll(".text-truncate.cursor-pointer");
console.log("Profile Openers:", profileOpeners);
var profileOpener = null;
profileOpeners.forEach(function(opener) {
if (opener.textContent.trim() === username) {
profileOpener = opener;
}
});
console.log("Profile Opener:", profileOpener);
// Log chronometer element
var chronometer = document.querySelector("app-chronometer");
console.log("Chronometer:", chronometer);
// Log number element
var numberElement = profileOpener ? profileOpener.parentNode.querySelectorAll("span")[4] : null;
console.log("Number Element:", numberElement);
// Log profile opener parent
var profileOpenerParent = profileOpener ? profileOpener.parentNode.parentNode : null;
console.log("Profile Opener Parent:", profileOpenerParent);
// Log SVG element
var svgElement = profileOpenerParent ? profileOpenerParent.querySelector("circle[class*='circle-dark-stroked']") : null;
if (!svgElement && profileOpenerParent) {
svgElement = profileOpenerParent.querySelector("svg[class*='fa-xmark']");
}
console.log("SVG Element:", svgElement);
// Determine and log the player
var player = null;
if (svgElement && svgElement.closest("circle[class*='circle-dark-stroked']")) {
player = 'o'; // Player is playing as "O"
} else if (svgElement && svgElement.closest("svg[class*='fa-xmark']")) {
player = 'x'; // Player is playing as "X"
}
console.log("Player:", player);
// Log current element
var currentElement = chronometer || numberElement;
console.log("Current Element:", currentElement);
console.log("Logging complete for this iteration.\n");
});
} catch (error) {
console.error("Error in logBoardState:", error);
}
}
// Call logBoardState every 5 seconds
setInterval(logBoardState, 5000);
var player;
function initGame() {
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.target.id === 'tic-tac-toe-board') {
initAITurn();
}
});
});
observer.observe(document.getElementById('tic-tac-toe-board'), { attributes: true, childList: true, subtree: true });
}
function initAITurn() {
displayBoardAndPlayer();
var boardState = getBoardState();
var bestMove = findBestMove(boardState, player);
updateBoard(bestMove.row.toString() + bestMove.col.toString());
}
function findBestMove(board, player) {
console.log("Current player: " + player); // Debug statement to show the value of the player variable
var bestVal = -1000;
var bestMove = { row: -1, col: -1 };
for (var i = 0; i < 3; i++) {
for (var j = 0; j < 3; j++) {
if (board[i][j] === '_') {
board[i][j] = player;
var moveVal = minimax(board, 0, false, depth);
board[i][j] = '_';
if (moveVal > bestVal) {
bestMove.row = i;
bestMove.col = j;
bestVal = moveVal;
}
}
}
}
console.log("The value of the best Move is: " + bestVal);
return bestMove;
}
function displayBoardAndPlayer() {
var boardState = getBoardState();
console.log("Board State:");
boardState.forEach(function(row) {
console.log(row.join(' | '));
});
}
function getOpponent(player) {
return player === 'x' ? 'o' : 'x';
}
function minimax(board, depth, isMaximizingPlayer, maxDepth) {
var score = evaluateBoard(board);
if (depth === maxDepth) {
return evaluateBoard(board);
}
if (score === 10)
return score - depth;
if (score === -10)
return score + depth;
if (areMovesLeft(board) === false)
return 0;
if (isMaximizingPlayer) {
var best = -1000;
for (var i = 0; i < 3; i++) {
for (var j = 0; j < 3; j++) {
if (board[i][j] === '_') {
board[i][j] = player;
best = Math.max(best, minimax(board, depth + 1, !isMaximizingPlayer));
board[i][j] = '_';
}
}
}
return best;
} else {
var best = 1000;
for (var i = 0; i < 3; i++) {
for (var j = 0; j < 3; j++) {
if (board[i][j] === '_') {
board[i][j] = getOpponent(player);
best = Math.min(best, minimax(board, depth + 1, !isMaximizingPlayer));
board[i][j] = '_';
}
}
}
return best;
}
}
function evaluateBoard(board) {
// Check rows for victory
for (let row = 0; row < 3; row++) {
if (board[row][0] === board[row][1] && board[row][1] === board[row][2]) {
if (board[row][0] === player) return +10;
else if (board[row][0] !== '_') return -10;
}
}
// Check columns for victory
for (let col = 0; col < 3; col++) {
if (board[0][col] === board[1][col] && board[1][col] === board[2][col]) {
if (board[0][col] === player) return +10;
else if (board[0][col] !== '_') return -10;
}
}
// Check diagonals for victory
if (board[0][0] === board[1][1] && board[1][1] === board[2][2]) {
if (board[0][0] === player) return +10;
else if (board[0][0] !== '_') return -10;
}
if (board[0][2] === board[1][1] && board[1][1] === board[2][0]) {
if (board[0][2] === player) return +10;
else if (board[0][2] !== '_') return -10;
}
// If no one has won, return 0
return 0;
}
function areMovesLeft(board) {
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (board[i][j] === '_') return true;
}
}
return false;
}
setInterval(function() {
initAITurn();
}, 1000);
document.addEventListener('DOMContentLoaded', function() {
initGame();
});
})();