WikiRace Smart Solver Pro

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

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

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

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

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