Greasy Fork is available in English.

GeoGuessr ChatGPT Assistant

Analyzes GeoGuessr scenes using OpenAI Vision API

// ==UserScript==
// @name         GeoGuessr ChatGPT Assistant
// @namespace    http://tampermonkey.net/
// @version      2025-01-25
// @description  Analyzes GeoGuessr scenes using OpenAI Vision API
// @author       elmulinho
// @match        https://www.geoguessr.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=geoguessr.com
// @grant        GM_xmlhttpRequest
// @grant        window.navigator.mediaDevices
// @connect      api.openai.com
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // Configuration - Replace these with your values
    const CONFIG = {
        TIMEOUT: 5000,
        SAFE_MODE: false,
        OPENAI_API_KEY: 'YOUR-API-KEY',
        MODEL: 'gpt-4o',
        MAX_TOKENS: 2000,
        PROMPT: 'You are a Geoguessr bot. You will receive an image that is a round of Geoguessr. Your job is to provide the user with the correct answer to the given round. The possible countries: USA, Russia, Brazil, Indonesia, Australia, Mexico, Canada, Argentina, India, South Africa, Japan, Turkey, Peru, France, Spain, Chile, Colombia, Kazakhstan, Thailand, New Zealand, Philippines, Nigeria, Norway, Italy, Malaysia, United Kingdom, Kenya, Germany, Sweden, Ukraine, Romania',
        RESPONSE_FORMAT: {
            "type": "json_schema",
            "json_schema": {
                "name": "guess_response",
                "schema": {
                "type": "object",
                "properties": {
                    "country": {
                    "type": "string",
                    "description": "The country name based on the guess."
                    },
                    "province": {
                    "type": "string",
                    "description": "Province in the guessed country where you think the location is from."
                    },
                    "explanation": {
                    "type": "string",
                    "description": "Explanation detailing why this country was chosen and where in country could this location be."
                    },
                    "coordinates": {
                    "type": "object",
                    "properties": {
                        "latitude": {
                        "type": "number",
                        "description": "Latitude coordinate of the guessed country in the guessed province. They should be very precise, at least 5 decimal digits."
                        },
                        "longitude": {
                        "type": "number",
                        "description": "Longitude coordinate of the guessed country in the guessed province. They should be very precise, at least 5 decimal digits."
                        }
                    },
                    "required": [
                        "latitude",
                        "longitude"
                    ],
                    "additionalProperties": false
                    },
                    "confidence": {
                    "type": "number",
                    "description": "Confidence level of how sure you are in your guess expressed as a percentage."
                    }
                },
                "required": [
                    "country",
                    "province",
                    "explanation",
                    "coordinates",
                    "confidence"
                ],
                "additionalProperties": false
                },
                "strict": true
            }
        }
    };

    // Create info panel
    function createInfoPanel() {
        const panel = document.createElement('div');
        panel.style.cssText = `
            position: fixed;
            top: 10px;
            right: 10px;
            background: rgba(0, 0, 0, 0.8);
            color: white;
            padding: 15px;
            border-radius: 8px;
            font-family: Arial, sans-serif;
            z-index: 10000;
            max-width: 300px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.3);
        `;
        return panel;
    }

    // Update info panel with analysis results
    function updateInfoPanel(analysis) {
        // Remove existing panel if present
        const existingPanel = document.querySelector('#gpt-info-panel');
        if (existingPanel) {
            existingPanel.remove();
        }

        const panel = createInfoPanel();
        panel.id = 'gpt-info-panel';
        document.body.appendChild(panel);

        // Set timeout to remove panel after 5 seconds
        setTimeout(() => {
            panel.remove();
        }, CONFIG.TIMEOUT);

        const confidenceColor = analysis.confidence >= 80 ? '#4CAF50' :
                              analysis.confidence >= 50 ? '#FFC107' : '#F44336';

        panel.innerHTML = `
            <div style="margin-bottom: 10px; font-size: 16px; font-weight: bold;">
                ${analysis.country}${analysis.province ? ` - ${analysis.province}` : ''}
            </div>
            <div style="margin-bottom: 10px; font-size: 14px;">
                ${analysis.explanation}
            </div>
            <div style="display: flex; align-items: center; gap: 10px;">
                <div style="flex-grow: 1; height: 20px; background: #444; border-radius: 10px; overflow: hidden;">
                    <div style="width: ${analysis.confidence}%; height: 100%; background: ${confidenceColor};"></div>
                </div>
                <div style="font-size: 14px; font-weight: bold;">
                    ${Math.round(analysis.confidence)}%
                </div>
            </div>
        `;
    }

    // Function to find Street View canvas
    async function findStreetViewIframe(timeout = 10000) {
        const startTime = Date.now();

        while (Date.now() - startTime < timeout) {
            const canvases = document.querySelectorAll('canvas');
            const mainCanvas = Array.from(canvases).find(canvas =>
                canvas.className.includes('mapsConsumerUiSceneCoreScene__canvas') &&
                !canvas.className.includes('impressCanvas')
            );

            if (mainCanvas) {
                return mainCanvas;
            }

            await new Promise(resolve => setTimeout(resolve, 100));
        }

        return null;
    }

    // Function to capture screenshot
    async function captureScreenshot() {
        try {
            const streetViewIframe = await findStreetViewIframe();
            if (!streetViewIframe) {
                throw new Error('Street View iframe not found');
            }

            const rect = streetViewIframe.getBoundingClientRect();

            // Request screen capture
            const stream = await navigator.mediaDevices.getDisplayMedia({
                preferCurrentTab: true,
                video: {
                    width: rect.width,
                    height: rect.height
                }
            });

            // Create video element to capture the stream
            const video = document.createElement('video');
            video.srcObject = stream;
            await new Promise(resolve => video.onloadedmetadata = resolve);
            await video.play();

            // Create canvas and capture frame
            const tempCanvas = document.createElement('canvas');
            tempCanvas.width = video.videoWidth;
            tempCanvas.height = video.videoHeight;

            const ctx = tempCanvas.getContext('2d');
            ctx.drawImage(video, 0, 0);

            // Stop the stream
            stream.getTracks().forEach(track => track.stop());

            const screenshot = tempCanvas;

            const dataUrl = screenshot.toDataURL('image/png');
            return dataUrl;
        } catch (error) {
            console.error('❌ Error capturing screenshot:', error);
            return null;
        }
    }

    // Function to place marker on the map
    async function placeMarker(coordinates, safeMode) {
        const { latitude: lat, longitude: lng } = coordinates;

        let finalLat = lat;
        let finalLng = lng;

        if (safeMode) { // applying random values to received coordinates
            const sway = [Math.random() > 0.5, Math.random() > 0.5];
            const multiplier = Math.random() * 4;
            const horizontalAmount = Math.random() * multiplier;
            const verticalAmount = Math.random() * multiplier;
            finalLat = sway[0] ? lat + verticalAmount : lat - verticalAmount;
            finalLng = sway[1] ? lng + horizontalAmount : lng - horizontalAmount;
        }

        let element = document.querySelectorAll('[class^="guess-map_canvas__"]')[0];
        if (!element) {
            console.error('❌ Map canvas not found');
            return;
        }

        const latLngFns = {
            latLng: {
                lat: () => finalLat,
                lng: () => finalLng,
            }
        };

        try {
            // Fetching Map Element and Props to extract place function
            const reactKeys = Object.keys(element);
            const reactKey = reactKeys.find(key => key.startsWith("__reactFiber$"));
            const elementProps = element[reactKey];
            const mapElementClick = elementProps.return.return.memoizedProps.map.__e3_.click;
            const mapElementPropKey = Object.keys(mapElementClick)[0];
            const mapClickProps = mapElementClick[mapElementPropKey];
            const mapClickPropKeys = Object.keys(mapClickProps);

            for (let i = 0; i < mapClickPropKeys.length; i++) {
                if (typeof mapClickProps[mapClickPropKeys[i]] === "function") {
                    mapClickProps[mapClickPropKeys[i]](latLngFns);
                }
            }
        } catch (error) {
            console.error('❌ Error placing marker:', error);
        }
    }

    // Function to send image to OpenAI Vision API
    async function analyzeImage(imageData) {
        const base64Image = imageData.split(',')[1];

        const requestData = {
            model: CONFIG.MODEL,
            messages: [
                {
                    role: "user",
                    content: [
                        { type: "text", text: CONFIG.PROMPT },
                        { type: "image_url", image_url: { url: `data:image/jpeg;base64,${base64Image}` } }
                    ]
                }
            ],
            max_tokens: CONFIG.MAX_TOKENS,
            response_format: CONFIG.RESPONSE_FORMAT,
            store: true
        };

        try {
            const response = await fetch('https://api.openai.com/v1/chat/completions', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${CONFIG.OPENAI_API_KEY}`
                },
                body: JSON.stringify(requestData)
            });

            const data = await response.json();

            if (data.error) {
                console.error('❌ API Error:', data.error);
                return null;
            }

            return data.choices[0].message.content;
        } catch (error) {
            console.error('❌ Error analyzing image:', error);
            return null;
        }
    }

    // Function to wait for game elements to load
    async function waitForGame(timeout = 10000) {
        const startTime = Date.now();
        while (Date.now() - startTime < timeout) {
            const elements = document.querySelectorAll('iframe, div[class*="game"], div[class*="street"]');
            if (elements.length > 0) {
                console.log('✅ Game elements found');
                return true;
            }
            await new Promise(resolve => setTimeout(resolve, 100));
        }
        console.error('❌ Timeout waiting for game elements');
        return false;
    }

    // Event listener for key press
    document.addEventListener('keydown', async function(event) {
        if (event.key === '1') {
            console.log('🎮 Key "1" pressed - Starting process...');

            console.log('⏳ Waiting for game to load...');
            const gameLoaded = await waitForGame();
            if (!gameLoaded) {
                console.error('❌ Game not loaded');
                return;
            }
            // Debug log to show available elements
            console.log('Available elements:', {
                gameLayout: document.querySelector('.game-layout'),
                nextDiv: document.querySelector('#__next div[class*="game-layout"]'),
                streetView: document.querySelector('#street-view'),
                canvasContainer: document.querySelector('#canvas-container')
            });

            const screenshot = await captureScreenshot();
            if (screenshot) {
                console.log('✅ Screenshot captured successfully');
                const analysis = await analyzeImage(screenshot);
                if (analysis) {
                    try {
                        const parsedAnalysis = JSON.parse(analysis);
                        updateInfoPanel(parsedAnalysis);
                        console.log(`Coordinates: ${parsedAnalysis.coordinates.latitude}, ${parsedAnalysis.coordinates.longitude}`);
                        await placeMarker(parsedAnalysis.coordinates, CONFIG.SAFE_MODE);
                    } catch (e) {
                        console.error('❌ Error parsing JSON response:', e);
                    }
                } else {
                    console.error('❌ Analysis failed');
                }
            } else {
                console.error('❌ Screenshot capture failed');
            }
        }
    });
})();