WikiRace Smart Solver Pro

Advanced Beam-Search solver with Hide UI toggle and MIT license.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         WikiRace Smart Solver Pro
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  Advanced Beam-Search solver with Hide UI toggle and MIT license.
// @author       Gemini
// @match        *://*.wiki-race.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const API_URL = "https://en.wikipedia.org/w/api.php";
    let currentMaxDepth = 6;
    let currentBeamWidth = 45; 

    const STOP_WORDS = new Set(["the", "a", "an", "of", "in", "to", "and", "is", "was", "for", "on", "with", "at", "by", "from", "as", "it", "that", "this", "are", "be", "or", "its", "has", "had", "have", "were", "been", "not", "but", "which", "their", "also", "may", "can", "all", "into", "than", "other", "some", "such", "no", "if"]);
    const SKIP_PREFIXES = ["Wikipedia:", "WP:", "Help:", "Template:", "Talk:", "User:", "Category:", "File:", "Portal:", "Draft:", "Module:", "MediaWiki:", "Special:", "Template talk:"];

    function tokenize(text) {
        if (!text) return new Set();
        const words = text.toLowerCase().match(/[a-z]{2,}/g) || [];
        return new Set(words.filter(w => !STOP_WORDS.has(w)));
    }

    async function fetchAPI(params) {
        const url = new URL(API_URL);
        url.search = new URLSearchParams({ ...params, origin: '*', format: 'json', formatversion: 2 });
        try {
            const res = await fetch(url);
            return res.ok ? await res.json() : null;
        } catch (e) { return null; }
    }

    async function getLinks(title) {
        let links = [];
        let plcontinue = null;
        do {
            const params = { action: 'query', titles: title, prop: 'links', pllimit: 'max', plnamespace: 0 };
            if (plcontinue) params.plcontinue = plcontinue;
            const data = await fetchAPI(params);
            if (!data) break;
            const page = data.query?.pages?.[0];
            if (page?.links) {
                page.links.forEach(l => { 
                    if (!l.title.endsWith(" (disambiguation)") && !SKIP_PREFIXES.some(p => l.title.startsWith(p))) links.push(l.title); 
                });
            }
            plcontinue = data.continue?.plcontinue;
        } while (plcontinue);
        return links;
    }

    async function findLinkSection(article, targetLink) {
        const data = await fetchAPI({ action: 'parse', page: article, prop: 'text', redirects: 1 });
        if (!data?.parse?.text) return "Introduction";
        const doc = new DOMParser().parseFromString(data.parse.text, 'text/html');
        const targetHref = `/wiki/${targetLink.replace(/ /g, "_")}`;
        const linkElem = Array.from(doc.querySelectorAll('a')).find(a => a.getAttribute('href') === targetHref || a.title === targetLink);
        if (linkElem) {
            let curr = linkElem;
            while (curr && curr !== doc.body) {
                if (curr.previousElementSibling?.tagName.match(/^H[2-6]$/)) return curr.previousElementSibling.textContent.replace(/\[edit\]/g, '').trim();
                curr = curr.parentElement || curr.previousElementSibling;
            }
        }
        return "Lead / Introduction";
    }

    class RelevanceScorer {
        constructor(targetCtx) {
            this.targetTitle = targetCtx.raw_title.toLowerCase();
            this.targetWords = targetCtx.title_tokens;
            this.keywords = new Set([...this.targetWords, ...targetCtx.category_tokens, ...targetCtx.extract_tokens]);
        }
        score(linkTitle) {
            const linkLower = linkTitle.toLowerCase();
            const linkTokens = tokenize(linkTitle);
            let score = 0.0;
            if (linkLower === this.targetTitle) return 1000.0;
            const titleOverlap = [...linkTokens].filter(x => this.targetWords.has(x)).length;
            if (titleOverlap > 0) score += 30.0 * (titleOverlap / Math.max(this.targetWords.size, 1));
            const kwOverlap = [...linkTokens].filter(x => this.keywords.has(x)).length;
            score += 5.0 * kwOverlap;
            const hubs = ["united states", "europe", "history", "science", "geography"];
            if (hubs.some(h => linkLower.includes(h))) score += 2.0;
            return score;
        }
    }

    async function solveRace(start, end, statusCallback) {
        const startRes = await fetchAPI({ action: 'query', titles: start, redirects: 1 });
        const endRes = await fetchAPI({ action: 'query', titles: end, redirects: 1 });
        start = startRes?.query?.pages?.[0]?.title || start;
        end = endRes?.query?.pages?.[0]?.title || end;

        statusCallback("Analyzing target...");
        const [catData, extData] = await Promise.all([
            fetchAPI({ action: 'query', titles: end, prop: 'categories', cllimit: 'max', clshow: '!hidden' }),
            fetchAPI({ action: 'query', titles: end, prop: 'extracts', exintro: 1, explaintext: 1, exchars: 1500 })
        ]);
        
        let categories = new Set();
        catData?.query?.pages?.[0]?.categories?.forEach(c => tokenize(c.title.replace("Category:", "")).forEach(w => categories.add(w)));
        const scorer = new RelevanceScorer({ raw_title: end, title_tokens: tokenize(end), category_tokens: categories, extract_tokens: tokenize(extData?.query?.pages?.[0]?.extract || "") });

        let parents = { [start]: null };
        let frontier = [start];

        for (let depth = 1; depth <= currentMaxDepth + 1; depth++) {
            let candidates = [];
            statusCallback(`Searching Depth ${depth}...`);
            const currentBeam = (depth > currentMaxDepth) ? currentBeamWidth + 50 : currentBeamWidth;

            for (let title of frontier) {
                const links = await getLinks(title);
                for (let link of links) {
                    if (link === end) {
                        parents[link] = title;
                        let path = [], curr = end;
                        while (curr) { path.push(curr); curr = parents[curr]; }
                        path.reverse();
                        const sections = [];
                        for (let j = 0; j < path.length - 1; j++) sections.push(await findLinkSection(path[j], path[j+1]));
                        return { path, sections };
                    }
                    if (!(link in parents)) candidates.push({ score: scorer.score(link), link, parent: title });
                }
            }
            if (candidates.length === 0) break;
            candidates.sort((a, b) => b.score - a.score);
            frontier = candidates.slice(0, currentBeam).map(c => { parents[c.link] = c.parent; return c.link; });
        }
        return null;
    }

    function createUI() {
        const container = document.createElement('div');
        container.id = 'wr-container';
        container.style.cssText = `position: fixed; bottom: 20px; right: 20px; width: 320px; background: #1e1e2e; color: #cdd6f4; border-radius: 10px; padding: 15px; box-shadow: 0 4px 15px rgba(0,0,0,0.5); z-index: 999999; font-family: sans-serif; border: 1px solid #45475a; transition: all 0.3s ease;`;

        const minimized = document.createElement('div');
        minimized.id = 'wr-minimized';
        minimized.style.cssText = `position: fixed; bottom: 20px; right: 20px; width: 50px; height: 50px; background: #a6e3a1; border-radius: 50%; display: none; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 10px rgba(0,0,0,0.3); z-index: 999999; font-size: 20px;`;
        minimized.innerText = '🏁';

        container.innerHTML = `
            <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
                <h3 style="margin: 0; color: #a6e3a1; font-size: 16px;">🏁 WikiRace Bot</h3>
                <button id="wr-hide" style="background:none; border:none; color:#f38ba8; cursor:pointer; font-size:18px; font-weight:bold;">—</button>
            </div>
            <input type="text" id="wr-start" placeholder="Start Article" style="width: 100%; padding: 6px; margin-bottom: 8px; background: #313244; color: white; border: 1px solid #585b70; border-radius: 4px;">
            <input type="text" id="wr-end" placeholder="Target Article" style="width: 100%; padding: 6px; margin-bottom: 8px; background: #313244; color: white; border: 1px solid #585b70; border-radius: 4px;">
            <button id="wr-solve" style="width: 100%; padding: 10px; background: #89b4fa; color: #11111b; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">Find Path</button>
            <button id="wr-auto" style="width: 100%; padding: 6px; margin-top: 5px; background: #fab387; color: #11111b; border: none; border-radius: 4px; cursor: pointer; font-size: 11px;">Auto-Detect</button>
            <div id="wr-status" style="margin-top: 10px; font-size: 12px; color: #f9e2af; max-height: 150px; overflow-y: auto;">Ready.</div>
        `;

        document.body.appendChild(container);
        document.body.appendChild(minimized);

        document.getElementById('wr-hide').onclick = () => { container.style.display = 'none'; minimized.style.display = 'flex'; };
        minimized.onclick = () => { container.style.display = 'block'; minimized.style.display = 'none'; };

        document.getElementById('wr-auto').onclick = () => {
            const lS = document.querySelector('input[placeholder="From here..."]'), lE = document.querySelector('input[placeholder="To there..."]');
            if (lS?.value) document.getElementById('wr-start').value = lS.value;
            if (lE?.value) document.getElementById('wr-end').value = lE.value;
            const s = Array.from(document.querySelectorAll('*')).find(e => e.textContent.includes('Start:')), e = Array.from(document.querySelectorAll('*')).find(e => e.textContent.includes('Target:'));
            if (s) document.getElementById('wr-start').value = s.textContent.replace('Start:', '').trim();
            if (e) document.getElementById('wr-end').value = e.textContent.replace('Target:', '').trim();
        };

        document.getElementById('wr-solve').onclick = async () => {
            const s = document.getElementById('wr-start').value, e = document.getElementById('wr-end').value, btn = document.getElementById('wr-solve'), st = document.getElementById('wr-status');
            if (!s || !e) return;
            btn.disabled = true; btn.innerText = "SOLVING...";
            const res = await solveRace(s, e, m => st.innerText = m);
            if (res) {
                let out = `🏆 FOUND (${res.path.length - 1} clicks): \n` + res.path.map((p, i) => `${i+1}. ${p}${res.sections[i] ? ` (Section: ${res.sections[i]})` : ''}`).join('\n');
                st.innerText = out; alert(out);
            } else st.innerText = "❌ No path found.";
            btn.disabled = false; btn.innerText = "Find Path";
        };
    }
    createUI();
})();