WikiRace Smart Solver Pro

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

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

(I already have a user script manager, let me install it!)

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.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

المؤلف
jul!e@n
التثبيت اليومي
3
إجمالي التثبيت
4
التقييمات
0 0 0
الإصدار
1.4
تم إنشاؤه
21-04-2026
تم تحديثه
21-04-2026
الحجم
10.7 KB
الترخيص
MIT
ينطبق على

// ==UserScript==
// @name WikiRace Smart Solver
// @namespace http://tampermonkey.net/
// @version 1.2
// @description Beam-search solver for WikiRace with auto-detection and MIT license.
// @author Gemini
// @match *://*.wiki-race.com/*
// @grant none
// @license MIT
// ==/UserScript==

/*
MIT License

Copyright (c) 2026

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

(function() {
'use strict';

// ─── Configuration ────────────────────────────────────────────────────────────
const API_URL = "https://en.wikipedia.org/w/api.php";
const MAX_DEPTH = 6;
const BEAM_WIDTH = 40;

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:"
];

// ─── Utilities ────────────────────────────────────────────────────────────────
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)));
}

function isValidLink(title) {
if (!title || title.endsWith(" (disambiguation)")) return false;
return !SKIP_PREFIXES.some(prefix => title.startsWith(prefix));
}

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);
if (!res.ok) return null;
return await res.json();
} catch (e) {
console.error("WikiAPI Error:", e);
return null;
}
}

// ─── Wikipedia API Client ────────────────────────────────────────────────────
async function resolveTitle(title) {
const data = await fetchAPI({ action: 'query', titles: title, redirects: 1 });
return data?.query?.pages?.[0]?.title || title;
}

async function getArticleContext(title) {
const [catData, extData] = await Promise.all([
fetchAPI({ action: 'query', titles: title, prop: 'categories', cllimit: 'max', clshow: '!hidden' }),
fetchAPI({ action: 'query', titles: title, prop: 'extracts', exintro: 1, explaintext: 1, exchars: 1000 })
]);

let categories = new Set();
if (catData?.query?.pages?.[0]?.categories) {
catData.query.pages[0].categories.forEach(c => {
tokenize(c.title.replace("Category:", "")).forEach(w => categories.add(w));
});
}

let extract = extData?.query?.pages?.[0]?.extract || "";
return {
raw_title: title,
title_tokens: tokenize(title),
category_tokens: categories,
extract_tokens: tokenize(extract)
};
}

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 && page.links) {
page.links.forEach(l => {
if (isValidLink(l.title)) 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 "Unknown Section";

const html = data.parse.text;
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');

const targetHref = `/wiki/${targetLink.replace(/ /g, "_")}`;
const links = Array.from(doc.querySelectorAll('a'));

for (let a of links) {
const href = a.getAttribute('href');
if (href === targetHref || href?.startsWith(targetHref + "#") || a.title === targetLink) {
let curr = a;
while (curr && curr !== doc.body) {
if (curr.previousElementSibling && curr.previousElementSibling.tagName.match(/^H[2-6]$/)) {
return curr.previousElementSibling.textContent.replace(/\[edit\]/g, '').trim();
}
curr = curr.parentElement || curr.previousElementSibling;
}
return "Lead / Introduction";
}
}
return "Unknown Section";
}

// ─── Relevance Scorer ────────────────────────────────────────────────────────
class RelevanceScorer {
constructor(targetCtx, startCtx) {
this.targetTitle = targetCtx.raw_title.toLowerCase();
this.targetWords = targetCtx.title_tokens;
this.startWords = startCtx.title_tokens;
this.targetCats = targetCtx.category_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 100.0;

const titleOverlap = [...linkTokens].filter(x => this.targetWords.has(x)).length;
if (titleOverlap > 0) score += 15.0 * (titleOverlap / Math.max(this.targetWords.size, 1));

if (this.targetTitle.includes(linkLower) || linkLower.includes(this.targetTitle)) score += 10.0;

const kwOverlap = [...linkTokens].filter(x => this.keywords.has(x)).length;
if (kwOverlap > 0) score += 3.0 * kwOverlap;

const catOverlap = [...linkTokens].filter(x => this.targetCats.has(x)).length;
score += 2.0 * catOverlap;

const hubs = ["united states", "education", "history", "geography", "list of", "culture", "science", "national"];
if (hubs.some(h => linkLower.includes(h))) score += 0.5;

const startOnly = [...linkTokens].filter(x => this.startWords.has(x) && !this.targetWords.has(x)).length;
if (startOnly > 0) score -= 1.0;

return score;
}
}

// ─── Solver ──────────────────────────────────────────────────────────────────
async function solveRace(start, end, statusCallback) {
start = await resolveTitle(start);
end = await resolveTitle(end);

if (start === end) return { path: [start], sections: [] };

statusCallback("Analyzing target context...");
const [startCtx, targetCtx] = await Promise.all([ getArticleContext(start), getArticleContext(end) ]);
const scorer = new RelevanceScorer(targetCtx, startCtx);

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

for (let depth = 1; depth <= MAX_DEPTH; depth++) {
statusCallback(`Searching Depth ${depth}...`);
let candidates = [];

for (let i = 0; i < frontier.length; i++) {
const title = frontier[i];
statusCallback(`Depth ${depth}: Fetching links for ${title}... (${i+1}/${frontier.length})`);
const links = await getLinks(title);

for (let link of links) {
if (link === end) {
parents[link] = title;
const path = [];
let curr = end;
while (curr) { path.push(curr); curr = parents[curr]; }
path.reverse();

statusCallback("Path found! Locating sections...");
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);
const kept = candidates.slice(0, BEAM_WIDTH);

frontier = [];
for (let c of kept) {
if (!(c.link in parents)) {
parents[c.link] = c.parent;
frontier.push(c.link);
}
}
}
return null;
}

// ─── UI / DOM Integration ────────────────────────────────────────────────────
function createUI() {
const container = document.createElement('div');
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; font-size: 14px; border: 1px solid #45475a;
`;

container.innerHTML = `

🏁 WikiRace Bot


Start Article


Target Article

Find Path
Auto-Detect from Page

`;

document.body.appendChild(container);

document.getElementById('wr-auto').addEventListener('click', () => {
let foundStart = "";
let foundEnd = "";

const lobbyStartInput = document.querySelector('input[placeholder="From here..."]');
const lobbyEndInput = document.querySelector('input[placeholder="To there..."]');

if (lobbyStartInput && lobbyStartInput.value) {
foundStart = lobbyStartInput.value;
}
if (lobbyEndInput && lobbyEndInput.value) {
foundEnd = lobbyEndInput.value;
}

if (!foundStart) {
const possibleStarts = Array.from(document.querySelectorAll('div, span, h1, h2, h3, p')).filter(e => e.textContent.includes('Start:'));
if (possibleStarts.length > 0) {
foundStart = possibleStarts[possibleStarts.length - 1].textContent.replace('Start:', '').trim();
}
}
if (!foundEnd) {
const possibleEnds = Array.from(document.querySelectorAll('div, span, h1, h2, h3, p')).filter(e => e.textContent.includes('Target:') || e.textContent.includes('Destination:'));
if (possibleEnds.length > 0) {
foundEnd = possibleEnds[possibleEnds.length - 1].textContent.replace(/(Target:|Destination:)/g, '').trim();
}
}

if (foundStart) document.getElementById('wr-start').value = foundStart;
if (foundEnd) document.getElementById('wr-end').value = foundEnd;

document.getElementById('wr-status').innerText = "Attempted auto-detect. Verify words above.";
});

document.getElementById('wr-solve').addEventListener('click', async () => {
const start = document.getElementById('wr-start').value.trim();
const end = document.getElementById('wr-end').value.trim();
const statusEl = document.getElementById('wr-status');
const btn = document.getElementById('wr-solve');

if (!start || !end) {
statusEl.innerText = "Please provide both start and end articles.";
return;
}

btn.disabled = true;
btn.style.background = "#585b70";

try {
const result = await solveRace(start, end, (msg) => { statusEl.innerText = msg; });

if (result) {
let out = `🏆 PATH FOUND (${result.path.length - 1} clicks):\n\n`;
for (let i = 0; i < result.path.length; i++) {
out += `${i === 0 ? '🟢' : (i === result.path.length - 1 ? '🔴' : '→')} ${result.path[i]}\n`;
if (i < result.sections.length) {
out += ` 📌 Section: "${result.sections[i]}"\n`;
}
}
statusEl.innerText = out;
alert(out);
} else {
statusEl.innerText = "❌ No path found within limits.";
}
} catch (err) {
console.error(err);
statusEl.innerText = "Error occurred. Check console.";
} finally {
btn.disabled = false;
btn.style.background = "#89b4fa";
}
});
}

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createUI);
} else {
createUI();
}
})();