WikiRace Smart Solver Pro

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

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

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