vocabulary.com bot

2/21/2025, 7:27:07 PM

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.

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.

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

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