Torn Keno Hot Numbers Tracker

Tracks Keno numbers played in Torn City and displays the most frequent "hot" numbers within the tokens div.

当前为 2025-06-24 提交的版本,查看 最新版本

// ==UserScript==
// @name         Torn Keno Hot Numbers Tracker
// @namespace    torn.keno.hotnumbers.tracker
// @version      1.2
// @description  Tracks Keno numbers played in Torn City and displays the most frequent "hot" numbers within the tokens div.
// @author       eaksquad
// @match        https://www.torn.com/page.php?sid=keno*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest // Included as a fallback if direct URL fetching is needed, though AJAX is prioritized.
// @license      MIT
// ==/UserScript==
 
(function() {
    'use strict';
 
    // --- Constants ---
    const SCRIPT_NAME = "Keno Hot Numbers Tracker";
    const SCRIPT_VERSION = "1.2"; // Updated internal version
    const KENO_NUMBERS_STORAGE_KEY = 'kenoNumberFrequencies'; // Key for storing frequencies in GM_getValue
    const DISPLAY_CONTAINER_ID = 'keno-hot-numbers-content'; // ID for the new div INSIDE alertZoneInner
    const TARGET_PARENT_CONTAINER_ID = 'alertZoneInner'; // The div where our display will be placed
    const MAX_HOT_NUMBERS_TO_SHOW = 10; // How many of the hottest numbers to display
    const KENO_NUMBER_RANGE_MIN = 1;    // Minimum valid Keno number
    const KENO_NUMBER_RANGE_MAX = 80;   // Maximum valid Keno number
 
    console.log(`[${SCRIPT_NAME} v${SCRIPT_VERSION}] Script loading.`);
 
    // --- State Management ---
    // This object will store the counts of each Keno number played.
    // Example: { 5: 12, 18: 8, 77: 15 }
    let kenoNumberFrequencies = {};
 
    // --- Utility Functions ---
 
    /**
     * Loads saved Keno number frequencies from storage.
     * Initializes kenoNumberFrequencies with stored data or an empty object if none exists.
     */
    function loadFrequencies() {
        try {
            const storedData = GM_getValue(KENO_NUMBERS_STORAGE_KEY, '{}');
            kenoNumberFrequencies = JSON.parse(storedData);
            // Ensure the loaded data is a valid object. If not (e.g., corrupted data), reset to an empty object.
            if (typeof kenoNumberFrequencies !== 'object' || kenoNumberFrequencies === null) {
                console.warn(`[${SCRIPT_NAME}] Stored frequencies data was invalid. Resetting.`);
                kenoNumberFrequencies = {};
            }
            console.log(`[${SCRIPT_NAME}] Loaded ${Object.keys(kenoNumberFrequencies).length} number frequencies.`);
        } catch (error) {
            console.error(`[${SCRIPT_NAME}] Error loading frequencies:`, error);
            kenoNumberFrequencies = {}; // Fallback to empty object on any error.
        }
    }
 
    /**
     * Saves the current Keno number frequencies to storage.
     */
    function saveFrequencies() {
        try {
            GM_setValue(KENO_NUMBERS_STORAGE_KEY, JSON.stringify(kenoNumberFrequencies));
        } catch (error) {
            console.error(`[${SCRIPT_NAME}] Error saving frequencies:`, error);
        }
    }
 
    /**
     * Updates the frequencies count for each number drawn in a Keno game.
     * This function is called when Keno result data is successfully captured.
     * @param {number[]} drawnNumbers - An array of numbers drawn in the latest game session.
     */
    function updateFrequencies(drawnNumbers) {
        if (!Array.isArray(drawnNumbers) || drawnNumbers.length === 0) {
            console.warn(`[${SCRIPT_NAME}] No valid drawn numbers provided for frequency update.`);
            return;
        }
 
        let changesMade = false; // Flag to track if any frequencies were actually updated.
        drawnNumbers.forEach(num => {
            // Validate that the number is within the expected Keno range (1-80).
            if (num >= KENO_NUMBER_RANGE_MIN && num <= KENO_NUMBER_RANGE_MAX) {
                kenoNumberFrequencies[num] = (kenoNumberFrequencies[num] || 0) + 1; // Increment count, or initialize to 1 if first time seen.
                changesMade = true;
            } else {
                console.warn(`[${SCRIPT_NAME}] Encountered out-of-range number (${num}). Ignoring.`);
            }
        });
 
        if (changesMade) {
            saveFrequencies(); // Save the updated frequencies to persistent storage.
            updateDisplay(); // Refresh the display to show the latest "hot" numbers.
            console.log(`[${SCRIPT_NAME}] Frequencies updated. Total unique numbers tracked: ${Object.keys(kenoNumberFrequencies).length}`);
        }
    }
 
    /**
     * Creates or updates the display element that shows the hottest Keno numbers.
     * This function is responsible for querying the current frequencies, sorting them,
     * and rendering them in the designated div inside alertZoneInner.
     */
    function updateDisplay() {
        // Target the specific child div for Keno numbers within alertZoneInner.
        const displayContainer = document.getElementById(DISPLAY_CONTAINER_ID);
        if (!displayContainer) {
            console.warn(`[${SCRIPT_NAME}] Display container #${DISPLAY_CONTAINER_ID} not found! Cannot update.`);
            // Attempt to re-initialize if it was missed or destroyed.
            initializeDisplay(); // This might fix it if the parent container exists but the child doesn't.
            return;
        }
 
        // Convert the frequencies object into an array of [number, count] pairs.
        // Then sort this array based on the count in descending order to get the hottest numbers first.
        const sortedNumbers = Object.entries(kenoNumberFrequencies)
            .sort(([, countA], [, countB]) => countB - countA) // Sort by count (descending)
            .slice(0, MAX_HOT_NUMBERS_TO_SHOW); // Limit to the top N hottest numbers
 
        // If no numbers have been tracked yet, display a placeholder message.
        if (sortedNumbers.length === 0) {
            displayContainer.innerHTML = '<em>No Keno data tracked yet. Play a game!</em>';
            return;
        }
 
        // Format the sorted numbers for display. Example: "68 (15) | 53 (12) | ..."
        // Wrap numbers and counts in spans for potential styling.
        const displayHtml = sortedNumbers.map(([num, count]) =>
            `<span class="keno-number-entry">${num}</span> <span class="keno-number-count">(${count})</span>`
        ).join(' | '); // Join entries with a separator.
 
        // Update the container's HTML with the formatted string.
        displayContainer.innerHTML = `<strong>Hottest Keno Numbers:</strong> ${displayHtml}`;
    }
 
    /**
     * Initializes the display element in the DOM by finding the parent container
     * and creating the child div for Keno numbers.
     */
    function initializeDisplay() {
        // Find the target parent container specified by the user.
        const parentContainer = document.getElementById(TARGET_PARENT_CONTAINER_ID);
 
        if (!parentContainer) {
            console.warn(`[${SCRIPT_NAME}] Target parent container #${TARGET_PARENT_CONTAINER_ID} not found. Keno tracker display will not be initialized.`);
            return; // Exit if the parent container doesn't exist.
        }
 
        // Inject CSS styles for the Keno numbers display within the alertZoneInner.
        // NOTE: The 'Unexpected token ;' error often stems from CSS syntax issues within GM_addStyle template literals.
        // Using double quotes for the font name is a common fix for such parsing ambiguities.
        GM_addStyle(`
            /* Styles for the Keno numbers display div */
            #${DISPLAY_CONTAINER_ID} {
                color: #e0e0e0; /* Light text color */
                padding: 5px 0 5px 0; /* Top/bottom padding, no left/right needed as it's inside another div */
                font-size: 13px; /* Slightly smaller font for better fit */
                font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; /* Modern font stack - Changed to double quotes for 'Segoe UI' */
                display: block; /* Ensure it takes its own line */
                white-space: normal; /* Allow text content to wrap naturally */
                text-align: center; /* Center the content */
                margin-top: 5px; /* Space from the "tokens: 11" text above */
                word-break: break-word; /* Break long words/numbers */
            }
            #${DISPLAY_CONTAINER_ID} strong {
                color: #00e5ff; /* Bright cyan for the title */
                font-weight: 600;
                margin-right: 5px; /* Space between title and numbers */
            }
            #${DISPLAY_CONTAINER_ID} .keno-number-entry {
                font-weight: bold;
                color: #00e5ff; /* Bright cyan for the numbers */
                margin-right: 3px; /* Small space after number */
            }
            #${DISPLAY_CONTAINER_ID} .keno-number-count {
                font-size: 0.85em;
                color: #bdbdbd; /* Lighter grey for counts */
            }
        `);
 
        // Create the Keno numbers display div if it doesn't already exist.
        if (!document.getElementById(DISPLAY_CONTAINER_ID)) {
            const div = document.createElement('div');
            div.id = DISPLAY_CONTAINER_ID;
            // Initial placeholder text.
            div.innerHTML = '<em>Waiting for Keno results...</em>';
            parentContainer.appendChild(div); // Append it to the target parent container.
            console.log(`[${SCRIPT_NAME}] Keno display container #${DISPLAY_CONTAINER_ID} created.`);
        }
 
        // Update the display with the current frequencies once the element is ready.
        updateDisplay();
    }
 
    // --- AJAX Interception Logic ---
    // This function intercepts network requests made by the browser to capture Keno result data.
    // It relies on heuristics to identify relevant responses.
    const originalFetch = window.fetch; // Store the original fetch function.
 
    // Override window.fetch to intercept requests.
    window.fetch = function(...args) {
        // Call the original fetch function and get its promise.
        return originalFetch.apply(this, args).then(response => {
            // Important: Clone the response. This allows the original response to be used elsewhere
            // (like by Torn's own scripts) while we read from the clone.
            const clonedResponse = response.clone();
 
            // Attempt to parse the cloned response as JSON.
            // This will only succeed if the response body is valid JSON.
            clonedResponse.json().then(data => {
                // Heuristic check: Does this JSON data look like Keno results?
                // We check for the presence of `randomNumbers` (an array) and `slotturns` (a number),
                // which are key indicators from the example data.
                if (data && typeof data === 'object' && Array.isArray(data.randomNumbers) && data.hasOwnProperty('slotturns') && data.hasOwnProperty('winnings')) {
                    console.log(`[${SCRIPT_NAME}] Detected Keno result data.`);
                    // If it looks like Keno results, update our frequencies and display.
                    updateFrequencies(data.randomNumbers);
                }
            }).catch(err => {
                // If parsing fails (e.g., response is HTML, not JSON, or not valid JSON),
                // this catch block will execute. We can ignore these errors as they are expected
                // for most network requests that aren't Keno results.
                // console.warn(`[${SCRIPT_NAME}] Response was not JSON or not Keno results:`, err);
            });
 
            return response; // Always return the original response object so the fetch chain continues normally.
        });
    };
    
    // --- Direct URL Fetching (Fallback/Alternative - Less Likely to be needed) ---
    // The user mentioned a URL like `page.php?rfcv=...` appearing. If Keno results
    // are *not* loaded via AJAX but by navigating to such a URL, we'd need GM_xmlhttpRequest.
    // This is less common for dynamic game results.
    // The following is a placeholder for how it might be implemented if AJAX fails.
    /*
    let lastProcessedRfcv = null; // To avoid re-processing the same result URL.
 
    function checkForRfcvResult() {
        const currentUrl = window.location.href;
        const urlParams = new URLSearchParams(currentUrl.split('?')[1] || '');
        const rfcvParam = urlParams.get('rfcv');
 
        // Check if we are on a result page (has rfcv) and if it's a new one we haven't processed.
        if (rfcvParam && rfcvParam !== lastProcessedRfcv) {
            lastProcessedRfcv = rfcvParam; // Mark this RFVC as processed.
            
            // Construct the full URL, assuming it's on the same origin.
            const resultUrl = `${window.location.origin}/page.php?rfcv=${rfcvParam}`;
            console.log(`[${SCRIPT_NAME}] Detected new rfcv parameter. Attempting to fetch Keno results from: ${resultUrl}`);
 
            GM_xmlhttpRequest({
                method: "GET",
                url: resultUrl,
                headers: { "User-Agent": "Mozilla/5.0" }, // Standard browser User-Agent
                onload: function(response) {
                    try {
                        const htmlContent = response.responseText;
                        const jsonMatch = htmlContent.match(/<script[^>]*>\s*({.*?})\s*<\/script>/s);
                        
                        if (jsonMatch && jsonMatch[1]) {
                            const jsonData = JSON.parse(jsonMatch[1]);
                            if (jsonData && typeof jsonData === 'object' && Array.isArray(jsonData.randomNumbers) && jsonData.hasOwnProperty('slotturns')) {
                                console.log(`[${SCRIPT_NAME}] Successfully fetched and parsed Keno data from rfcv URL.`);
                                updateFrequencies(jsonData.randomNumbers);
                            } else {
                                console.warn(`[${SCRIPT_NAME}] Parsed JSON from rfcv URL did not match expected Keno structure.`);
                            }
                        } else {
                            console.warn(`[${SCRIPT_NAME}] Could not find embedded JSON data in the rfcv URL response.`);
                        }
                    } catch (e) {
                        console.error(`[${SCRIPT_NAME}] Error processing rfcv URL response:`, e);
                    }
                },
                onerror: function(err) {
                    console.error(`[${SCRIPT_NAME}] Failed to fetch rfcv URL ${resultUrl}:`, err);
                }
            });
        }
    }
    */
 
    // --- Initial Setup ---
    loadFrequencies(); // Load existing data when the script starts.
    initializeDisplay(); // Create and populate the display element within alertZoneInner.
 
    // The fetch interception handles AJAX calls automatically.
    // The checkForRfcvResult function is commented out, as AJAX interception is the primary method.
    // If AJAX fails, you would uncomment the line below and potentially adapt the JSON extraction logic within it.
    // checkForRfcvResult(); 
 
    // MutationObserver to detect changes in the DOM that might indicate new Keno results are loaded (e.g. if page reloads dynamically without a full navigation change, or if the AJAX logic fails)
    // This is a potential fallback if the fetch interception is somehow missed.
    // NOTE: The MutationObserver is a fallback and might be less efficient or trigger more often than necessary.
    // The primary Keno data detection is via the window.fetch override.
    const observer = new MutationObserver((mutations) => {
        mutations.forEach(mutation => {
            if (mutation.type === 'childList' && mutation.addedNodes && mutation.addedNodes.length > 0) {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === 1) { // Element node
                        // Look for elements that are likely to contain Keno result data or trigger result updates.
                        // This is heuristic and might need tuning.
                        // A broad check for JSON-like content might be too noisy.
                        // A more specific check could look for specific classes if known.
                        // For now, we'll assume that if `alertZoneInner` (or its child display div) is updated, it might be from a Keno result.
                        // This is less direct than fetch interception.
                        if (node.id === TARGET_PARENT_CONTAINER_ID || (node.querySelector && node.querySelector(`#${DISPLAY_CONTAINER_ID}`))) {
                             console.log(`[${SCRIPT_NAME}] DOM mutation detected potentially related to Keno results.`);
                             // Re-update display in case the structure changed or data was missed.
                             updateDisplay();
                        }
                    }
                });
            }
        });
    });
    // Observe the body for added nodes that might contain Keno results or update our display target.
    observer.observe(document.body, {
        childList: true,
        subtree: true
    });
 
 
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址