WikiRace Smart Solver Pro

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

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==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();
})();