Keno Heatmap

Tracks Keno random numbers and displays a heat map with light gray to dark red gradient

// ==UserScript==
// @name         Keno Heatmap
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @description  Tracks Keno random numbers and displays a heat map with light gray to dark red gradient
// @author       Allenone[2033011]
// @match        https://www.torn.com/page.php?sid=keno
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
    'use strict';

    const win = (unsafeWindow || window);
    const TARGET_URL_BASE = "torn.com/page.php";

    // Initialize frequency cache for numbers 1-80
    function initializeFrequencyCache() {
        let frequency = GM_getValue('kenoNumberFrequency', null);
        if (!frequency) {
            frequency = {};
            for (let i = 1; i <= 80; i++) {
                frequency[i] = 0;
            }
            GM_setValue('kenoNumberFrequency', frequency);
        }
        return frequency;
    }

    // Update frequency cache with new random numbers
    function updateFrequencyCache(randomNumbers) {
        let frequency = initializeFrequencyCache();
        randomNumbers.forEach(num => {
            if (num >= 1 && num <= 80) {
                frequency[num] = (frequency[num] || 0) + 1;
            }
        });
        GM_setValue('kenoNumberFrequency', frequency);
        updateHotspotMap(frequency);
    }

    // Interpolate between two RGB colors
    function interpolateColor(startColor, endColor, factor) {
        const result = startColor.slice();
        for (let i = 0; i < 3; i++) {
            result[i] = Math.round(startColor[i] + (endColor[i] - startColor[i]) * factor);
        }
        return `rgb(${result[0]}, ${result[1]}, ${result[2]})`;
    }

    // Update the hotspot map with color gradient from light gray to dark red
    function updateHotspotMap(frequency) {
        const startColor = [211, 211, 211]; // Light gray (#D3D3D3)
        const endColor = [139, 0, 0]; // Dark red (#8B0000)
        const maxFrequency = Math.max(...Object.values(frequency), 1); // Avoid division by 0

        const style = document.createElement('style');
        style.textContent = `
            #boardContainer span {
                border: 1px solid #ccc;
                border-radius: 4px;
                box-sizing: border-box;
                transition: background-color 0.3s;
            }
        `;
        document.head.appendChild(style);

        for (let i = 1; i <= 80; i++) {
            const element = document.getElementById(`keno_item_${i}`);
            if (element) {
                const freq = frequency[i] || 0;
                const factor = freq / maxFrequency; // Normalize frequency
                const color = interpolateColor(startColor, endColor, factor);
                element.style.backgroundColor = color;
                element.style.color = 'black';
            }
        }
    }

    // Observe DOM for boardContainer and initialize hotspot map
    function initializeHotspotOnBoardLoad() {
        const targetNode = document.body;
        const observer = new MutationObserver((mutations, obs) => {
            const boardContainer = document.getElementById('boardContainer');
            if (boardContainer) {
                updateHotspotMap(initializeFrequencyCache());
                obs.disconnect(); // Stop observing once board is found
            }
        });
        observer.observe(targetNode, {
            childList: true,
            subtree: true
        });
    }

    // Start observing for boardContainer
    initializeHotspotOnBoardLoad();

    const originalOpen = XMLHttpRequest.prototype.open;
    const originalSend = XMLHttpRequest.prototype.send;

    XMLHttpRequest.prototype.open = function(method, url, async, user, pass) {
        this._url = url;
        return originalOpen.apply(this, arguments);
    };

    XMLHttpRequest.prototype.send = function(body) {
        const isTargetUrl = this._url && this._url.includes(TARGET_URL_BASE);
        if (isTargetUrl) {
            const xhr = this;

            const handleResponse = function(event) {
                if (xhr.readyState === 4 && xhr.status === 200 && xhr.responseText) {
                    try {
                        const json = JSON.parse(xhr.responseText);
                        if (json.matches !== undefined && json.winnings !== undefined) {
                            //console.log(JSON.stringify(json, null, 2));
                            if (json.randomNumbers && Array.isArray(json.randomNumbers)) {
                                updateFrequencyCache(json.randomNumbers);
                            }
                        }
                    } catch (err) {
                    }
                }
            };

            xhr.addEventListener('load', handleResponse);
        }
        return originalSend.apply(this, arguments);
    };
})();