Connect 4 AI for papergames

Adds an AI player to Connect 4 on papergames.io

目前為 2024-10-27 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Connect 4 AI for papergames
// @namespace    https://github.com/longkidkoolstar
// @version      0.1.1
// @description  Adds an AI player to Connect 4 on papergames.io
// @author       longkidkoolstar
// @icon         https://th.bing.com/th/id/R.2ea02f33df030351e0ea9bd6df0db744?rik=Pnmqtc4WLvL0ow&pid=ImgRaw&r=0
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @match        https://papergames.io/*
// @license      none
// @grant        GM.xmlHttpRequest
// @grant        GM.setValue
// @grant        GM.getValue
// ==/UserScript==


(async function() {
    'use strict';

    var username = await GM.getValue('username');

    if (!username) {
        username = prompt('Please enter your Papergames username (case-sensitive):');
       await GM.setValue('username', username);
    }

    var player;
    var prevChronometerValue = '';
    var moveHistory = [];
    var lastBoardState = [];
    var aiTurn = false;
    var processingMove = false;
    var moveCooldown = false;

    function resetVariables() {
        player = undefined;
        prevChronometerValue = '';
        moveHistory = [];
        lastBoardState = [];
        aiTurn = false;
        processingMove = false;
        moveCooldown = false;
        console.log("Variables reset to default states");
    }
    function checkForResetButtons() {
        var playOnlineButton = document.querySelector("button.btn-secondary.flex-grow-1");
        var leaveRoomButton = document.querySelector("button.btn-light.ng-tns-c189-7");
        var customResetButton = document.querySelector("button.btn.btn-outline-dark.ng-tns-c497539356-18.ng-star-inserted");
    
        if (playOnlineButton || leaveRoomButton || customResetButton) {
            resetVariables();
        }
    }

    function updateBoard() {
        var profileOpeners = document.querySelectorAll(".text-truncate.cursor-pointer");
        var profileOpener = Array.from(profileOpeners).find(opener => opener.textContent.trim() === username);

        var chronometer = document.querySelector("app-chronometer");
        var numberElement;

        if (profileOpener) {
            var profileParent = profileOpener.parentNode;
            numberElement = profileOpener.parentNode.querySelectorAll("span")[4];

            var profileOpenerParent = profileOpener.parentNode.parentNode;
            var svgElementDark = profileOpenerParent.querySelector("circle.circle-dark");
            var svgElementLight = profileOpenerParent.querySelector("circle.circle-light");

            if (svgElementDark) {
                player = 'R';
            } else if (svgElementLight) {
                player = 'Y';
            }
        }

        var currentElement = chronometer || numberElement;
        if (currentElement && currentElement.textContent !== prevChronometerValue && profileOpener) {
            prevChronometerValue = currentElement.textContent;
            console.log("AI's turn detected. Waiting 2 seconds before making API move...");
            aiTurn = true;
            setTimeout(() => {
                if (!moveCooldown) {
                    console.log("Making API move...");
                    makeAPIMove();
                }
            }, 1500);
        } else {
            aiTurn = false;
            console.log("Waiting for AI's turn...");
        }
    }

    function getBoardState() {
        const boardContainer = document.querySelector(".grid.size6x7");
        if (!boardContainer) {
            console.error("Board container not found");
            return [];
        }
    
        let boardState = [];
    
        // Iterate over cells in a more flexible way
        for (let row = 1; row <= 6; row++) {
            let rowState = [];
            for (let col = 1; col <= 7; col++) {
                // Use a selector that matches the class names correctly
                const cellSelector = `.grid-item.cell-${row}-${col}`;
                const cell = boardContainer.querySelector(cellSelector);
                if (cell) {
                    // Check the circle class names to determine the cell's state
                    const circle = cell.querySelector("circle");
                    if (circle) {
                        if (circle.classList.contains("circle-dark")) {
                            rowState.push("R");
                        } else if (circle.classList.contains("circle-light")) {
                            rowState.push("Y");
                        } else {
                            rowState.push("E");
                        }
                    } else {
                        rowState.push("E");
                    }
                } else {
                    console.error(`Cell not found: ${cellSelector}`);
                    rowState.push("E");
                }
            }
            boardState.push(rowState);
        }
    
        return boardState;
    }
    
    function detectNewMove() {
        const currentBoardState = getBoardState();
        let newMove = false;
    
        for (let row = 0; row < 6; row++) {
            for (let col = 0; col < 7; col++) {
                if (lastBoardState[row] && lastBoardState[row][col] === 'E' && currentBoardState[row][col] !== 'E') {
                    moveHistory.push(col + 1);
                    newMove = true;
                }
            }
        }
    
        lastBoardState = currentBoardState;
        return newMove;
    }
    
    function simulateCellClick(column) {
        console.log(`Attempting to click on column ${column}`);
        const boardContainer = document.querySelector(".grid.size6x7");
        if (!boardContainer) {
            console.error("Board container not found");
            return;
        }
    
        for (let row = 5; row >= 0; row--) {
            const cellSelector = `.cell-${row}-${column}`;
            const cell = boardContainer.querySelector(cellSelector);
            if (cell && cell.classList.contains('selectable')) {
                console.log(`Found selectable cell at row ${row}, column ${column}`);
                setTimeout(() => {
                    console.log(`Dispatching click event on row ${row}, column ${column}`);
                    var event = new MouseEvent('click', {
                        bubbles: true,
                        cancelable: true,
                    });
                    cell.dispatchEvent(event);
                    console.log(`Click event dispatched on row ${row}, column ${column}`);
                    processingMove = false; // Move processed, reset flag
                    moveCooldown = true; // Set cooldown
                    setTimeout(() => moveCooldown = false, 2000); // Reset cooldown after 2 seconds
                }, 1000);
                return;
            }
        }
        console.log(`No selectable cell found in column ${column}`);
        processingMove = false; // No valid move found, reset flag
    }

    function makeAPIMove() {
        if (!aiTurn || processingMove) return; // Ensure AI turn and not processing another move
        processingMove = true; // Set flag to indicate move processing

        detectNewMove();
        console.log("Move history:", moveHistory);

        let pos = moveHistory.join("");
        console.log("API position string:", pos);
        console.log("Move count:", pos.length);

        const apiUrl = `https://connect4.gamesolver.org/solve?pos=${pos}`;
        console.log("API URL:", apiUrl);

        GM.xmlHttpRequest({
            method: "GET",
            url: apiUrl,
            onload: function(response) {
                console.log("API response received:", response.responseText);
                const data = JSON.parse(response.responseText);
                console.log("Parsed API data:", data);
                const scores = data.score;
                console.log("Move scores:", scores);

                let bestMove = -1;
                let bestScore = -Infinity;
                for (let i = 0; i < scores.length; i++) {
                    if (scores[i] !== 100 && scores[i] > bestScore) {
                        bestScore = scores[i];
                        bestMove = i;
                    }
                }

                console.log("Best move (column):", bestMove);
                if (bestMove !== -1) {
                    simulateCellClick(bestMove);
                } else {
                    console.log("No valid moves available");
                    processingMove = false; // No valid move, reset flag
                }
            },
            onerror: function(error) {
                console.error("API request failed:", error);
                processingMove = false; // API request failed, reset flag
            }
        });
    }

    function initAITurn() {
        console.log("Player: ", player);
        const boardState = getBoardState();
        console.log("Board State: ", boardState);

        if (!player) {
            for (let row of boardState) {
                for (let cell of row) {
                    if (cell !== "E") {
                        player = cell === "R" ? "Y" : "R";
                        break;
                    }
                }
                if (player) break;
            }
        }
    }

function logout() {
    localStorage.removeItem('username');
    location.reload();
}

function createLogoutButton() {
    $('<button>')
        .text('Logout')
        .addClass('btn btn-secondary mb-2 ng-star-inserted')
        .css({
            position: 'fixed',
            bottom: '20px',
            right: '20px',
            zIndex: '9999',
            color: 'white'
        })
        .on('click', logout)
        .on('mouseover', function() { $(this).css('opacity', '0.5'); })
        .on('mouseout', function() { $(this).css('opacity', '1'); })
        .appendTo('body');
}

$(function() {
    createLogoutButton();

    var $dropdownContainer = $('<div>')
        .css({
            position: 'fixed',
            bottom: '20px',
            left: '20px',
            zIndex: '9998',
            backgroundColor: '#1b2837',
            border: '1px solid #18bc9c',
            borderRadius: '5px'
        })
        .appendTo('body');

    var $toggleButton = $('<button>')
        .text('Settings')
        .addClass('btn btn-secondary mb-2 ng-star-inserted')
        .css({
            padding: '5px 10px',
            border: 'none',
            backgroundColor: '#007bff',
            color: 'white',
            borderRadius: '5px'
        })
        .on('mouseover', function() { $(this).css('opacity', '0.5'); })
        .on('mouseout', function() { $(this).css('opacity', '1'); })
        .appendTo($dropdownContainer);

    var $dropdownContent = $('<div>')
        .css({
            display: 'none',
            padding: '8px'
        })
        .appendTo($dropdownContainer);

    var $autoQueueTab = $('<div>')
        .text('Auto Queue')
        .css({
            padding: '5px 0',
            cursor: 'pointer'
        })
        .appendTo($dropdownContent);

    var $autoQueueSettings = $('<div>')
        .css('padding', '10px')
        .appendTo($dropdownContent);

    var isAutoQueueOn = false;

    var $autoQueueToggleButton = $('<button>')
        .text('Auto Queue Off')
        .addClass('btn btn-secondary mb-2 ng-star-inserted')
        .css({
            marginTop: '10px',
            backgroundColor: 'red',
            color: 'white'
        })
        .on('click', toggleAutoQueue)
        .appendTo($autoQueueSettings);

    function toggleAutoQueue() {
        isAutoQueueOn = !isAutoQueueOn;
        localStorage.setItem('isToggled', isAutoQueueOn);
        $autoQueueToggleButton.text(isAutoQueueOn ? 'Auto Queue On' : 'Auto Queue Off')
            .css('backgroundColor', isAutoQueueOn ? 'green' : 'red');
    }

    function clickLeaveRoomButton() {
        $("button.btn-light.ng-tns-c189-7").click();
    }

    function clickPlayOnlineButton() {
        $("button.btn-secondary.flex-grow-1").click();
    }

    function checkButtonsPeriodically() {
        if (isAutoQueueOn) {
            clickLeaveRoomButton();
            clickPlayOnlineButton();
        }
    }

    setInterval(checkButtonsPeriodically, 1000);

    let previousNumber = null;

    function trackAndClickIfDifferent() {
        const $spanElement = $('app-count-down span');
        if ($spanElement.length) {
            const number = parseInt($spanElement.text(), 10);
            if (!isNaN(number) && previousNumber !== null && number !== previousNumber && isAutoQueueOn) {
                $spanElement.click();
            }
            previousNumber = number;
        }
    }

    setInterval(trackAndClickIfDifferent, 1000);

    $toggleButton.on('click', function() {
        $dropdownContent.toggle();
    });
});

//---GUI



    setInterval(function() {
        updateBoard();
        initAITurn();
    }, 1000);

//Checking If the game is over so it can reset variables
setInterval(function() {
    checkForResetButtons();
}, 500);


    function displayAIBoard() {
        const boardState = getBoardState();
        console.log("Current board state:");
        boardState.forEach(row => {
            console.log(row.join(" | "));
        });
    }

    setInterval(displayAIBoard, 1000);
    setInterval(detectNewMove, 100);

    console.log("Connect 4 AI script loaded and running");
})();