// ==UserScript==
// @name vocabulary.com bot
// @namespace Violentmonkey Scripts
// @match https://www.vocabulary.com/lists/*/practice*
// @grant none
// @version 1.0
// @author -
// @description 2/21/2025, 7:27:07 PM
// @license GPL-3.0-or-later
// ==/UserScript==
/*
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
(function () {
'use strict';
// -- state for pausing
let paused = false;
// -- create a small overlay to toggle pause and display extra info
function createpauseoverlay() {
const div = document.createElement('div');
div.id = 'pause-overlay';
div.style.position = 'fixed';
div.style.top = '10px';
div.style.left = '10px';
div.style.zIndex = '9999';
div.style.background = '#333';
div.style.color = '#fff';
div.style.padding = '8px';
div.style.cursor = 'pointer';
div.style.borderRadius = '4px';
// pause button
const pauseText = document.createElement('div');
pauseText.innerText = 'pause script';
pauseText.style.marginBottom = '5px';
pauseText.style.cursor = 'pointer';
pauseText.addEventListener('click', () => {
paused = !paused;
pauseText.innerText = paused ? 'resume script' : 'pause script';
console.log(paused ? 'script paused' : 'script resumed');
});
// dynamic display area (shared for all q types)
const infoDisplay = document.createElement('div');
infoDisplay.id = 'info-display';
infoDisplay.style.fontSize = '12px';
infoDisplay.style.color = '#ddd';
infoDisplay.innerHTML = `
<div id="qtype-display">Q Type: N/A</div>
<div id="extra-info">Info: N/A</div>
`;
div.appendChild(pauseText);
div.appendChild(infoDisplay);
document.body.appendChild(div);
}
function updateOverlay(qtype, info) {
const qtypeDisplay = document.getElementById('qtype-display');
const extraInfo = document.getElementById('extra-info');
if (qtypeDisplay) qtypeDisplay.innerText = `Q Type: ${qtype}`;
if (extraInfo) extraInfo.innerText = `Info: ${info || 'N/A'}`;
}
// -- "synonym" fetcher using vocabulary.com dictionary page
async function fetchsynonyms(word) {
const url = `https://www.vocabulary.com/dictionary/${encodeURIComponent(word)}`;
try {
const resp = await fetch(url);
if (!resp.ok) throw new Error('Failed to fetch vocabulary.com page');
const text = await resp.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
let synonyms = [];
const instances = doc.querySelectorAll("div.div-replace-dl.instances");
instances.forEach(instance => {
const detailSpan = instance.querySelector("span.detail");
if (detailSpan && detailSpan.textContent.trim().toLowerCase().includes("synonyms")) {
instance.querySelectorAll("a.word").forEach(a => {
synonyms.push(a.textContent.trim().toLowerCase());
});
}
});
return synonyms;
} catch (err) {
console.error("Error fetching synonyms from vocabulary.com:", err);
return [];
}
}
// -- helper to see if a choice is correct
function iscorrect(choice) {
return choice.className.includes('correct');
}
// -- helper: attempt synonyms only if qtype == 'S'
async function handleTypeS(curq, qlist, choices) {
const synonyms = await fetchsynonyms(curq.q.toLowerCase());
updateOverlay('S', synonyms.length ? `Synonyms: ${synonyms.join(', ')}` : 'No synonyms found.');
if (!synonyms.length) {
console.log('no synonyms found. falling back.');
return false;
}
for (let i = 0; i < choices.length; i++) {
const text = choices[i].innerText.trim().toLowerCase();
if (synonyms.includes(text)) {
console.log(`clicking synonym match: ${text}`);
choices[i].click();
if (iscorrect(choices[i])) {
qlist[curq.q] = text;
localStorage.practiceLists = JSON.stringify(plists);
console.log(`recorded: "${curq.q}" -> "${text}"`);
clicknext();
}
return true;
}
}
return false;
}
// -- Modular handler for question type 'D' (definition-based questions)
async function handleTypeD(curQ, qList, choices, pLists) {
const allDefs = JSON.parse(localStorage.getItem('words&defs') || '[]');
const entry = allDefs.find(e => e.word?.toLowerCase() === curQ.q.toLowerCase());
if (!entry || !entry.definition) {
console.log(`no local definition found for "${curQ.q}"`);
updateOverlay('D', 'No definition found.');
return false;
}
updateOverlay('D', `Definition: ${entry.definition}`);
// naive token matching
const defTokens = entry.definition.toLowerCase().split(/\W+/);
let bestIndex = -1;
let bestScore = -1;
for (let i = 0; i < choices.length; i++) {
const choiceTokens = choices[i].innerText.trim().toLowerCase().split(/\W+/);
let score = choiceTokens.filter(t => defTokens.includes(t)).length;
if (score > bestScore) {
bestScore = score;
bestIndex = i;
}
}
if (bestIndex !== -1) {
choices[bestIndex].click();
console.log(`attempting definition match: "${choices[bestIndex].innerText.trim()}" (score: ${bestScore})`);
if (iscorrect(choices[bestIndex])) {
qList[curQ.q] = choices[bestIndex].innerText.trim();
localStorage.practiceLists = JSON.stringify(pLists);
return true;
}
}
return false;
}
// -- Modular handler for question type 'F' (fill-based questions)
async function handleTypeF(curq, qlist, choices, pLists) {
const allDefs = JSON.parse(localStorage.getItem('words&defs') || '[]');
const knownWords = allDefs.map(e => e.word?.toLowerCase()).filter(Boolean);
// Convert HTMLCollection to an array so we can safely use .map()
const choiceArray = Array.from(choices);
const matchedWords = choiceArray
.map((c, i) => ({ text: c.innerText.trim().toLowerCase(), index: i }))
.filter(item => knownWords.includes(item.text));
// Update the overlay
updateOverlay('F', matchedWords.length ? `Matched: ${matchedWords.map(m => m.text).join(', ')}` : 'No match found.');
// If exactly one match, click it
if (matchedWords.length === 1) {
const index = matchedWords[0].index;
choices[index].click();
console.log(`F-type guess: matched known word "${matchedWords[0].text}"`);
if (iscorrect(choices[index])) {
qlist[curq.q] = matchedWords[0].text;
localStorage.practiceLists = JSON.stringify(pLists);
return true;
}
}
return false;
}
// -- main object to store question-to-answer mappings
function practicelist(id) {
this.id = id;
this.qtyped = {};
this.qtypes = {};
this.qtypep = {};
this.qtypeh = {};
this.qtypel = {};
this.qtypea = {};
this.qtypef = {};
this.qtypei = {};
this.qtypeg = {};
}
// -- read page context
const parts = window.location.href.split('/');
const ispractice = parts[3] === 'lists';
const practiceid = parts[4];
const stor = window.localStorage;
// -- load or create practice lists
const plists = stor.practiceLists ? JSON.parse(stor.practiceLists) : {};
if (!plists[practiceid]) {
plists[practiceid] = new practicelist(practiceid);
stor.practiceLists = JSON.stringify(plists);
}
const curlist = plists[practiceid];
console.log(`curlist: ${curlist.id}`);
const keyword = ispractice ? '.question' : '.box-question';
const keytypeindex = ispractice ? 4 : 5;
let lastq = null;
let triedindices = [];
let recordedtried = false;
// -- map question types to correct sub-objects
function getqlist(list, t) {
const map = {
'S': list.qtypes,
'D': list.qtyped,
'P': list.qtypep,
'H': list.qtypeh,
'L': list.qtypel,
'A': list.qtypea,
'F': list.qtypef,
'I': list.qtypei,
'G': list.qtypeg,
};
return map[t.toUpperCase()];
}
// -- fill in blank type
function answertypet(curq) {
const ans = curq.querySelector('.complete').children[0].innerText;
curq.querySelector('input').value = ans;
curq.querySelector('.spellit').click();
}
// -- click next
function clicknext() {
const btn = ispractice ? document.querySelector('.next') : document.querySelector('.btn-next');
if (btn) btn.click();
}
// -- helper: extracts an "answer" string from the choice (especially for images)
function extractanswer(choice, qtype) {
return qtype === 'I'
? choice.style.backgroundImage.split('/')[5]
: choice.innerText.trim();
}
// -- main loop
setInterval(answerquestion, 300);
async function answerquestion() {
if (paused) return; // skip logic if paused
const qnodes = document.querySelectorAll(keyword);
if (!qnodes.length) return;
const curq = qnodes[qnodes.length - 1];
const classes = curq.classList[1] || '';
const qtype = classes.charAt(keytypeindex).toUpperCase();
// Update overlay displays
updateOverlay(qtype, 'Loading...');
// Type 'T' is fill-in-the-blank
if (qtype === 'T') {
answertypet(curq);
clicknext();
return;
}
// Parse question text for various types
if (qtype === 'P' || qtype === 'L' || qtype === 'H') {
curq.q = curq.querySelector('.sentence').children[0].innerText;
} else if (qtype === 'F') {
curq.q = curq.querySelector('.sentence').innerText.split(' ')[0];
} else if (qtype === 'I') {
curq.q = ispractice
? curq.querySelector('.wrapper').innerText.split('\n')[1]
: curq.querySelector('.box-word').innerText.split('\n')[1];
} else if (qtype === 'G') {
curq.q = curq.querySelector('.questionContent').style.backgroundImage.split('/')[5];
} else {
curq.q = curq.querySelector('.instructions strong').innerText;
}
// Reset if new question
if (lastq !== curq.q) {
lastq = curq.q;
triedindices = [];
recordedtried = false;
}
const qlist = getqlist(curlist, qtype);
if (!qlist) return;
const choices = curq.querySelector('.choices').children;
// If we have a recorded answer, try it first
if (qlist.hasOwnProperty(curq.q)) {
updateOverlay(qtype, `Using recorded knowledge: "${qlist[curq.q]}"`);
const stored = qlist[curq.q];
let found = -1;
for (let i = 0; i < choices.length; i++) {
if (choices[i].innerText.trim() === stored) {
found = i;
break;
}
}
if (found !== -1) {
if (!recordedtried) {
choices[found].click();
console.log(`clicked recorded answer: "${stored}"`);
recordedtried = true;
return;
} else {
console.log(`recorded answer for "${curq.q}" failed. Removing & trying synonyms or random.`);
delete qlist[curq.q];
}
} else {
console.log(`recorded answer not found in choices, removing & trying synonyms or random.`);
delete qlist[curq.q];
}
}
// Modular handling: attempt qtype-specific logic
if (qtype === 'S') {
const handled = await handleTypeS(curq, qlist, choices);
if (handled) return;
}
if (qtype === 'D') {
const handled = await handleTypeD(curq, qlist, choices, plists);
if (handled) { clicknext(); return; }
}
if (qtype === 'F') {
const handled = await handleTypeF(curq, qlist, choices, plists);
if (handled) { clicknext(); return; }
}
// Fallback: sequential guess method as the last resort
let available = [];
for (let i = 0; i < choices.length; i++) {
if (!triedindices.includes(i)) available.push(i);
}
if (!available.length) {
triedindices = [];
available = Array.from({ length: choices.length }, (_, i) => i);
}
const r = available[Math.floor(Math.random() * available.length)];
choices[r].click();
if (iscorrect(choices[r])) {
const ans = extractanswer(choices[r], qtype);
qlist[curq.q] = ans;
stor.practiceLists = JSON.stringify(plists);
console.log(`recorded: "${curq.q}" -> "${ans}"`);
triedindices = [];
recordedtried = false;
clicknext();
} else {
triedindices.push(r);
}
}
// -- initialize the pause overlay
createpauseoverlay();
})();