TypeRacer Racer (v3.4)

[IMPORTANT] This is a non-functional, educational tool to demonstrate the vulnerability of image-based CAPTCHAs. It is intentionally designed to FAIL the CAPTCHA and DOES NOT provide any gameplay advantage. Use for learning purposes only.

// ==UserScript==
// @name         TypeRacer Racer (v3.4)
// @namespace    http://tampermonkey.net/
// @version      3.4
// @description  [IMPORTANT] This is a non-functional, educational tool to demonstrate the vulnerability of image-based CAPTCHAs. It is intentionally designed to FAIL the CAPTCHA and DOES NOT provide any gameplay advantage. Use for learning purposes only.
// @author       https://github.com/ahm4dd
// @match        https://play.typeracer.com/*
// @require      https://unpkg.com/[email protected]/dist/tesseract.min.js
// @grant        GM_xmlhttpRequest
// @license      MIT
// ==/UserScript==

/*
================================================================================
==  EDUCATIONAL PROOF OF CONCEPT - NON-FUNCTIONAL BY DESIGN                  ==
================================================================================
This script is intended for developers to understand
how automated bots can approach bypassing simple image-based CAPTCHAs.

IT WILL NOT SOLVE THE CAPTCHA. The actual OCR-solving functionality has been removed.
Using tools to cheat on TypeRacer is against their Terms of Service. This
script is for learning, not for cheating.
*/

(function() {
    'use strict';

    // --- State Management ---
    let ocrWorker = null;
    let interruptController = { reject: null }; // Global controller to interrupt async operations like CAPTCHA solving.

    const state = {
        isActive: false,
        isTyping: false,
        isCaptchaVisible: false,
        wpm: 120,
        accuracy: 100,
        currentIndex: 0,
        raceText: '',
    };

    // --- Core Functions ---

    // Programmatically sets an input's value. This is needed because frameworks like React
    // don't always listen to direct .value property changes. This simulates a real input event.
    function setInputValue(element, text) {
        const prototype = Object.getPrototypeOf(element);
        const valueSetter = Object.getOwnPropertyDescriptor(prototype, 'value').set;
        valueSetter.call(element, text);
        element.dispatchEvent(new Event('input', { bubbles: true }));
    }

    async function startTyping() {
        if (!state.isActive || state.isTyping || state.isCaptchaVisible) return;

        const inputField = document.querySelector('.txtInput');
        if (!inputField) return;

        // Figure out where we are in the race, in case of a page refresh or starting mid-race.
        const wordsTyped = document.querySelectorAll('.txtInput-unfocused > span[class=""]');
        let prefixLength = 0;
        if (wordsTyped.length > 0) {
            let currentTypedText = '';
            wordsTyped.forEach(span => { currentTypedText += span.textContent; });
            if (state.raceText.length > currentTypedText.length) {
                currentTypedText += ' ';
            }
            prefixLength = currentTypedText.length;
        }

        state.currentIndex = prefixLength + inputField.value.length;
        inputField.focus();
        state.isTyping = true;
        updateStatus('Typing...');

        for (let i = state.currentIndex; i < state.raceText.length; i++) {
            if (!state.isActive || state.isCaptchaVisible) {
                state.isTyping = false;
                updateStatus(state.isCaptchaVisible ? 'Solving...' : 'Paused');
                return;
            }

            state.currentIndex = i;
            const char = state.raceText[i];

            // Simulate a typo based on accuracy setting.
            if (Math.random() * 100 > state.accuracy && char !== ' ' && state.raceText[i - 1] !== ' ') {
                updateStatus('Correcting...');
                setInputValue(inputField, inputField.value + String.fromCharCode(97 + Math.floor(Math.random() * 26)));
                await new Promise(resolve => setTimeout(resolve, 150));

                // "Backspace" to the start of the current word to fix the typo.
                const lastSpaceIndex = state.raceText.lastIndexOf(' ', state.currentIndex) + 1;
                const backspaceCount = inputField.value.length;
                const correctedValue = inputField.value.slice(0, -backspaceCount);
                await new Promise(resolve => setTimeout(resolve, 80 + Math.random() * 50));
                setInputValue(inputField, correctedValue);
                await new Promise(resolve => setTimeout(resolve, 150));

                i = lastSpaceIndex - 1; // Reset loop to the beginning of the messed-up word.
                updateStatus('Typing...');
                continue;
            }

            // Calculate typing delay based on WPM. The randomness makes it look more human.
            const delay = (60 / (state.wpm * 5)) * 1000 * (1 + (Math.random() - 0.5) * 0.4);
            await new Promise(resolve => setTimeout(resolve, delay));
            setInputValue(inputField, inputField.value + char);
        }

        state.isTyping = false;
        updateStatus('Finished');
    }

    // Creates a promise that can be externally rejected by our interruptController.
    function createInterruptiblePromise() {
        return new Promise((_, reject) => {
            interruptController.reject = reject;
        });
    }

    // Utility to wait for an element to appear in the DOM before proceeding.
    function waitForElement(selector, timeout = 3000) {
        return new Promise((resolve, reject) => {
            const intervalTime = 100;
            const endTime = Date.now() + timeout;
            const intervalId = setInterval(() => {
                const element = document.querySelector(selector);
                if (element) {
                    clearInterval(intervalId);
                    resolve(element);
                } else if (Date.now() > endTime) {
                    clearInterval(intervalId);
                    reject(new Error(`Element "${selector}" not found within ${timeout}ms.`));
                }
            }, intervalTime);
        });
    }

    // --- OCR and CAPTCHA Solver ---
    async function initializeOcrWorker() {
        updateStatus("Initializing OCR...");
        console.log("Initializing Tesseract worker...");
        ocrWorker = Tesseract.createWorker({
            logger: m => console.log(m.status, `${(m.progress * 100).toFixed(0)}%`)
        });
        await ocrWorker.load();
        await ocrWorker.loadLanguage('eng');
        await ocrWorker.initialize('eng');
        console.log("Tesseract worker initialized and ready.");
        updateStatus(state.isActive ? "Waiting for race" : "Idle");
    }

    async function solveCaptchaImage(imageUrl) {
        if (!ocrWorker) throw new Error("OCR Worker not initialized.");
        updateStatus('Analyzing Image...');
        const { data: { text } } = await ocrWorker.recognize(imageUrl);
        // Clean up common OCR errors and formatting issues.
        return text.replace(/\n/g, ' ').replace(/[^a-zA-Z0-9\s.,?!'"-]/g, '').trim();
    }

    async function typeCaptchaText(element, text) {
        updateStatus('Typing CAPTCHA...');
        for (const char of text) {
            // Check before each character if the user has stopped the bot.
            if (!state.isActive) throw new Error("CAPTCHA typing interrupted by user.");
            const delay = (60 / (state.wpm * 5)) * 1000 * (1 + (Math.random() - 0.5) * 0.3);
            await new Promise(resolve => setTimeout(resolve, delay));
            setInputValue(element, element.value + char);
        }
    }

    async function handleCaptchaAppearance() {
        if (state.isCaptchaVisible) return;
        state.isCaptchaVisible = true;
        console.warn("CAPTCHA detected!");
        updateStatus('Solving CAPTCHA...');
        try {
            const interruptPromise = createInterruptiblePromise();
            // This allows the user to stop the bot in the middle of solving a CAPTCHA.
            const race = (promise) => Promise.race([promise, interruptPromise]);

            const captchaImg = await race(waitForElement('img.challengeImg'));
            const captchaInput = await race(waitForElement('textarea.challengeTextArea'));
            const submitButton = await race(waitForElement('button.gwt-Button'));

            const recognizedText = await race(solveCaptchaImage(captchaImg.src));
            console.log(`OCR Result: "${recognizedText}"`);

            if (recognizedText && recognizedText.length > 2) {
                await typeCaptchaText(captchaInput, recognizedText);
                await new Promise(resolve => setTimeout(resolve, 300));
                if (state.isActive) submitButton.click();
            } else {
                throw new Error("OCR returned little or no text.");
            }
        } catch (error) {
            console.log(`CAPTCHA process aborted: ${error.message}`);
            updateStatus(state.isActive ? "Waiting for race" : "Paused");
            state.isCaptchaVisible = false;
        }
    }

    function handleCaptchaDismissal() {
        if (!state.isCaptchaVisible) return;
        console.log("CAPTCHA solved. Resuming race.");
        state.isCaptchaVisible = false;
        updateStatus('Resuming...');
        if (state.isActive) startTyping();
    }

    function extractRaceText() {
        const textSpans = document.querySelectorAll('[unselectable="on"]');
        if (!textSpans || textSpans.length === 0) return null;
        let fullText = '';
        textSpans.forEach(span => { fullText += span.textContent; });
        // TypeRacer uses non-breaking spaces (\u00A0), so we convert them to regular spaces.
        return fullText.replace(/\u00A0/g, ' ');
    }

    function resetForNewRace() {
        state.isTyping = false;
        state.currentIndex = 0;
        state.raceText = '';
        updateStatus(state.isActive ? 'Waiting for race' : 'Idle');
    }

    function handleRaceStart() {
        if (state.isTyping || state.isCaptchaVisible) return;
        resetForNewRace();
        const newText = extractRaceText();
        if (newText) {
            state.raceText = newText;
            if (state.isActive) startTyping();
        }
    }

    // Use a MutationObserver to react to game state changes efficiently.
    function initializeObserver() {
        const observer = new MutationObserver(mutations => {
            const newRaceText = extractRaceText();
            if (newRaceText && newRaceText !== state.raceText && document.querySelector(".txtInput")) {
                handleRaceStart();
                return;
            }

            for (const mutation of mutations) {
                for (const addedNode of mutation.addedNodes) {
                    if (addedNode.nodeType === 1 && addedNode.querySelector('img[src*="challenge?"]')) {
                        handleCaptchaAppearance();
                        return;
                    }
                }
                for (const removedNode of mutation.removedNodes) {
                    if (removedNode.nodeType === 1 && removedNode.querySelector('img[src*="challenge?"]')) {
                        handleCaptchaDismissal();
                        return;
                    }
                }
                if (mutation.target.className && typeof mutation.target.className == "string" && mutation.target.className.includes("gameStatusLabel")) {
                    const statusText = mutation.target.textContent;
                    if (statusText.includes("The race has ended") || statusText.includes("You finished")) {
                        if (state.raceText !== "") resetForNewRace();
                    }
                }
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    // --- UI and Event Handlers ---
    function createUI() {
        const uiContainer = document.createElement("div");
        uiContainer.id = "tr-bot-ui";
        uiContainer.innerHTML = `<div class="tr-bot-title">TypeRacer Bot by ahm4dd</div><div class="tr-bot-buttons"><button id="tr-bot-toggle">Start</button><button id="tr-bot-clear">Clear</button></div><div class="tr-bot-slider"><label for="tr-bot-wpm">WPM: <span id="tr-bot-wpm-value">${state.wpm}</span></label><input type="range" id="tr-bot-wpm" min="30" max="300" value="${state.wpm}"></div><div class="tr-bot-slider"><label for="tr-bot-accuracy">Accuracy: <span id="tr-bot-accuracy-value">${state.accuracy}%</span></label><input type="range" id="tr-bot-accuracy" min="80" max="100" value="${state.accuracy}"></div><div class="tr-bot-status">Status: <span id="tr-bot-status-text">Idle</span></div>`;
        document.body.appendChild(uiContainer);

        const toggleButton = document.getElementById("tr-bot-toggle");
        const clearButton = document.getElementById("tr-bot-clear");

        toggleButton.addEventListener("click", () => {
            state.isActive = !state.isActive;
            toggleButton.textContent = state.isActive ? "Stop" : "Start";
            toggleButton.classList.toggle("active", state.isActive);
            if (state.isActive) {
                updateStatus("Waiting for race");
                if (document.querySelector(".txtInput") && state.raceText && !state.isTyping) {
                    startTyping();
                }
            } else {
                state.isTyping = false;
                if (interruptController.reject) interruptController.reject(new Error("Operation stopped by user."));
                updateStatus("Paused");
            }
        });

        clearButton.addEventListener("click", () => {
            console.log("User cleared state.");
            state.isActive = false;
            if (interruptController.reject) interruptController.reject(new Error("Operation cleared by user."));
            toggleButton.textContent = "Start";
            toggleButton.classList.remove("active");
            resetForNewRace();
        });

        document.getElementById("tr-bot-wpm").addEventListener("input", e => { state.wpm = parseInt(e.target.value, 10); document.getElementById("tr-bot-wpm-value").textContent = state.wpm; });
        document.getElementById("tr-bot-accuracy").addEventListener("input", e => { state.accuracy = parseInt(e.target.value, 10); document.getElementById("tr-bot-accuracy-value").textContent = `${state.accuracy}%`; });
    }

    function updateStatus(newStatus) {
        const statusElement = document.getElementById("tr-bot-status-text");
        if (statusElement) statusElement.textContent = newStatus;
    }

    function injectStyles() {
        const css = `#tr-bot-ui{position:fixed;bottom:20px;right:20px;background-color:#2a2a2e;color:#e2e2e2;border:1px solid #444;border-radius:8px;padding:15px;font-family:Arial,sans-serif;font-size:14px;z-index:9999;box-shadow:0 4px 10px rgba(0,0,0,0.4);width:220px}.tr-bot-title{font-weight:700;font-size:18px;text-align:center;margin-bottom:12px;color:#5cf}.tr-bot-buttons{display:flex;gap:10px;margin-bottom:10px}.tr-bot-buttons button{flex:1;padding:10px;border:none;border-radius:5px;color:#fff;font-weight:700;cursor:pointer;transition:background-color .2s}#tr-bot-toggle{background-color:#2e7d32}#tr-bot-toggle:hover{background-color:#388e3c}#tr-bot-toggle.active{background-color:#c62828}#tr-bot-toggle.active:hover{background-color:#d32f2f}#tr-bot-clear{background-color:#1e88e5}#tr-bot-clear:hover{background-color:#2196f3}.tr-bot-slider{margin:12px 0}.tr-bot-slider label{display:block;margin-bottom:5px}.tr-bot-slider input[type=range]{width:100%;cursor:pointer}.tr-bot-status{text-align:center;margin-top:8px;font-size:13px;color:#bbb}`;
        const styleElement = document.createElement("style");
        styleElement.innerText = css;
        document.head.appendChild(styleElement);
    }

    // Wait for the main game to load before injecting the UI and starting the bot.
    const loadingCheck = setInterval(() => {
        if (document.querySelector(".gameView")) {
            clearInterval(loadingCheck);
            injectStyles();
            createUI();
            initializeObserver();
            initializeOcrWorker();
            console.log("TypeRacer Pro Bot (v3.4 Interruptible) Initialized.");
        }
    }, 500);
})();