Tic Tac Toe AI for papergames

AI plays Tic-Tac-Toe for you on papergames.io. Have fun and destroy some nerds 😃!!

Install this script?
Author's suggested script

You may also like Connect 4 AI for papergames.

Install this script
// ==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();
    });
})();