// ==UserScript==
// @name Skribbl AutoGuesser
// @name:zh-CN Skribbl 自动猜词器
// @name:zh-TW Skribbl 自動猜詞器
// @name:hi Skribbl स्वतः अनुमान स्क्रिप्ट
// @name:es Skribbl Adivinador Automático
// @namespace http://tampermonkey.net/
// @version 1.08
// @description Automatically suggests guesses in Skribbl.io. Fast, easy, and effective.
// @description:zh-CN 自动在 Skribbl.io 中猜词,快速、简单、有效。
// @description:zh-TW 自動在 Skribbl.io 中猜詞,快速、簡單、有效。
// @description:hi Skribbl.io में शब्दों का अनुमान लगाने वाली तेज़ और आसान स्क्रिप्ट।
// @description:es Adivina palabras automáticamente en Skribbl.io de forma rápida y sencilla.
// @author Zach Kosove
// @supportURL https://github.com/zkisaboss/reorderedwordlist
// @match https://skribbl.io/*
// @icon https://skribbl.io/favicon.png
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// @compatible chrome
// @compatible firefox
// @compatible opera
// @compatible safari
// @compatible edge
// @downloadURL https://update.greasyfork.icu/scripts/503563/Skribbl%20AutoGuesser.user.js
// @updateURL https://update.greasyfork.icu/scripts/503563/Skribbl%20AutoGuesser.meta.js
// ==/UserScript==
(function() {
'use strict';
function createUI() {
const bottomUI = document.createElement('div');
bottomUI.id = 'bottom-ui';
bottomUI.innerHTML = `
`;
document.body.appendChild(bottomUI);
const ui = document.getElementById('bottom-ui');
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') ui.classList.add('hidden');
if (e.key === 'ArrowUp') ui.classList.remove('hidden');
});
}
createUI();
const correctAnswers = GM_getValue('correctAnswers', []);
async function fetchWords(url) {
const response = await fetch(url);
if (!response.ok) return [];
const text = await response.text();
return text.split('\n').filter(word => word !== '');
}
async function fetchAndStoreLatestWordlist() {
const words = await fetchWords('https://raw.githubusercontent.com/zkisaboss/reorderedwordlist/main/wordlist_test.txt');
words.forEach(word => {
if (!correctAnswers.includes(word)) correctAnswers.push(word);
});
}
fetchAndStoreLatestWordlist();
let myUsername = '';
function findUsername() {
const target = document.querySelector(".players-list");
if (!target) return;
const observer = new MutationObserver(() => {
myUsername = document.querySelector(".me").textContent.replace(" (You)", "")
observer.disconnect();
});
observer.observe(target, { childList: true });
}
findUsername();
function observeDrawingTurn() {
const target = document.querySelector('.words');
if (!target) return;
const observer = new MutationObserver(() => {
target.childNodes.forEach(word => {
const text = word.textContent.toLowerCase();
if (!correctAnswers.includes(text)) {
correctAnswers.push(text);
GM_setValue('correctAnswers', correctAnswers);
}
});
});
observer.observe(target, { childList: true });
}
observeDrawingTurn();
const remainingButton = document.getElementById('remaining-guesses');
const guessShelf = document.getElementById('guess-shelf');
let possibleWords = [];
const input = document.querySelector('#game-chat input[data-translate="placeholder"]');
function renderGuesses(words) {
guessShelf.innerHTML = '';
remainingButton.textContent = `Remaining Guesses: ${possibleWords.length}`;
words.forEach(word => {
const button = Object.assign(document.createElement('button'), {
className: 'ui-btn',
textContent: word,
onclick: () => {
input.value = word;
input.closest('form').dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
}
});
guessShelf.appendChild(button);
});
};
function generateGuesses() {
if (possibleWords.length === 1) {
input.value = possibleWords.shift();
input.closest('form').dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
}
const pattern = input.value.toLowerCase().trim();
const words = possibleWords.filter(word => word.startsWith(pattern));
renderGuesses(words);
}
function observeInput() {
input.addEventListener('input', generateGuesses);
input.addEventListener('keydown', ({ key }) => {
if (key === 'Enter') {
input.value = guessShelf.firstElementChild.innerText;
input.closest('form').dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
}
});
}
observeInput();
function storeAnswer(word) {
if (correctAnswers.includes(word)) {
const index = correctAnswers.indexOf(word);
const newIndex = Math.max(0, index - 1);
correctAnswers.splice(index, 1);
correctAnswers.splice(newIndex, 0, word);
}
else {
correctAnswers.push(word);
}
GM_setValue('correctAnswers', correctAnswers);
return [];
}
function filterHints(inputWords) {
const hints = Array.from(document.querySelectorAll('.hints .hint'));
const combined = hints.map(hint => hint.textContent === '_' ? '[a-z]' : hint.textContent).join('').toLowerCase();
const allUncovered = hints.every(hint => hint.classList.contains('uncover'));
if (allUncovered) return storeAnswer(combined);
const regex = new RegExp(`^${combined}$`, 'i');
return inputWords.filter(word => regex.test(word));
}
function observeHints() {
const target = document.querySelector('.hints .container');
if (!target) return;
const observer = new MutationObserver(() => {
possibleWords = filterHints(possibleWords);
generateGuesses();
});
observer.observe(target, { childList: true, subtree: true });
}
observeHints();
/*
// Levenshtein: O(n * m) time, O(n * m) space
// https://youtu.be/Dd_NgYVOdLk
function levenshteinDistance(a, b) {
const matrix = [];
for (let i = 0; i <= b.length; i++) matrix[i] = [i];
for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j - 1] + (b[i - 1] !== a[j - 1])
);
}
}
return matrix[b.length][a.length];
}
*/
// Banded Levenshtein: O(min(|n|, |m|) * k) time, O(min(m,n)) space
// https://www.baeldung.com/cs/levenshtein-distance-computation
function levenshteinDistance(a, b, k = 1) {
if (a.length > b.length) {
const t = a;
a = b;
b = t;
}
const m = a.length;
const n = b.length;
if (n - m > k) return -1;
if (m === 0) return n;
const rows = [ new Uint16Array(m + 1), new Uint16Array(m + 1) ];
for (let j = 0; j <= m; j++) rows[0][j] = j;
let flip = 0;
for (let i = 0; i < n; i++) {
flip ^= 1;
const curr = rows[flip];
const prev = rows[flip ^ 1];
curr[0] = i + 1;
const stripeStart = Math.max(1, i + 1 - k);
const stripeEnd = Math.min(m, i + 1 + k);
let rowMin = k + 1;
for (let j = stripeStart; j <= stripeEnd; j++) {
curr[j] = Math.min(
prev[j] + 1,
curr[j - 1] + 1,
prev[j - 1] + (a[j - 1] !== b[i])
);
if (curr[j] < rowMin) rowMin = curr[j];
}
if (rowMin > k) return -1;
}
return rows[flip][m];
}
let previousWords = [];
function handleChatMessage(messageNode) {
const messageColor = window.getComputedStyle(messageNode).color;
const message = messageNode.textContent;
if (messageColor === 'rgb(57, 117, 206)' && message.endsWith('is drawing now!')) {
possibleWords = filterHints(correctAnswers);
generateGuesses();
}
else if (message.includes(': ')) {
const [username, guess] = message.split(': ');
possibleWords = possibleWords.filter(word => word !== guess);
previousWords = possibleWords;
if (username === myUsername) {
possibleWords = possibleWords.filter(word => levenshteinDistance(word, guess) === -1);
}
generateGuesses();
}
else if (messageColor === 'rgb(226, 203, 0)' && message.endsWith('is close!')) {
const closeWord = message.replace(' is close!', '');
possibleWords = previousWords.filter(word => levenshteinDistance(word, closeWord) === 1);
generateGuesses();
}
}
function observeChat() {
const target = document.querySelector('.chat-content');
if (!target) return;
const observer = new MutationObserver(() => {
const lastMessage = target.lastElementChild;
if (lastMessage) handleChatMessage(lastMessage);
});
observer.observe(target, { childList: true });
}
observeChat();
let autoGuessInterval;
let autoGuessing = false;
function startAutoGuessing() {
if (!autoGuessing) return;
autoGuessInterval = setInterval(() => {
if (possibleWords.length > 0) {
input.value = possibleWords.shift();
input.closest('form').dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
}
}, 7500);
}
startAutoGuessing();
const autoGuessButton = document.getElementById('auto-guess');
function toggleAutoGuessing() {
autoGuessing = !autoGuessing;
autoGuessButton.innerHTML = `Auto Guess: ${autoGuessing ? 'ON' : 'OFF'}`;
if (autoGuessing) {
startAutoGuessing();
} else {
clearInterval(autoGuessInterval);
autoGuessInterval = null;
}
}
autoGuessButton.addEventListener('click', toggleAutoGuessing);
async function exportNewWords() {
const old = await fetchWords('https://raw.githubusercontent.com/zkisaboss/reorderedwordlist/main/wordlist.txt');
const newWords = correctAnswers.filter(word => !old.includes(word));
const blob = new Blob([newWords.join('\n')], { type: 'text/plain;charset=utf-8' });
const anchor = document.createElement('a');
anchor.href = URL.createObjectURL(blob);
anchor.download = 'newWords.txt';
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
}
const exportButton = document.getElementById('export-answers');
exportButton.addEventListener('click', exportNewWords);
const secretButton = document.getElementById('get-special');
function runSecret() {
const avatars = document.querySelectorAll('.avatar-container .avatar');
const interval = setInterval(() => {
let allSecretsVisible = true;
avatars.forEach(avatar => {
const secret = avatar.querySelector('.special');
if (getComputedStyle(secret).display === 'none') {
avatar.click();
allSecretsVisible = false;
}
});
if (allSecretsVisible) clearInterval(interval);
}, 15);
}
secretButton.addEventListener('click', runSecret);
function observeSecret() {
const target = document.getElementById('home');
if (!target) return;
const observer = new MutationObserver(() => {
secretButton.style.display = target.hasAttribute('style') ? 'none' : 'inline-block';
});
observer.observe(target, { attributes: true, attributeFilter: ['style'] });
}
observeSecret();
})();