Advanced Beam-Search solver with Hide UI toggle and MIT license.
// ==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();
})();