// ==UserScript==
// @name WordSleuth
// @namespace http://greasyfork.icu/en/users/1084087-fermion
// @version 0.5.3
// @description A script that helps you guess words in skribblio
// @author kueldev
// @match http*://www.skribbl.io/*
// @match http*://skribbl.io/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=skribbl.io
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// ==/UserScript==
(function() {
'use strict';
class WordSleuth {
constructor() {
this.correctAnswers = GM_getValue('correctAnswers', []);
this.possibleWords = [];
this.tempWords = [];
this.alreadyGuessed = [];
this.closeWord = '';
this.createParentElement();
this.createGuessElement();
this.createExportButton();
this.fetchAndStoreLatestWordlist();
this.observeHintsAndInput();
}
createParentElement() {
this.parentElement = document.createElement('div');
this.parentElement.style = 'position: fixed; bottom: 0; right: 0; width: 100%; height: auto;';
document.body.appendChild(this.parentElement);
}
createGuessElement() {
this.guessElem = document.createElement('div');
this.guessElem.style = 'padding: 10px; background-color: white; max-height: 200px; overflow-x: auto; white-space: nowrap; width: 100%;';
this.parentElement.appendChild(this.guessElem);
}
createExportButton() {
this.exportButton = document.createElement('button');
this.exportButton.innerHTML = 'Export Answers';
this.exportButton.style = 'position: absolute; bottom: calc(100% + 10px); right: 0; z-index: 9999; padding: 5px 10px; font-size: 12px; background-color: #333; color: #fff; border: none; border-radius: 5px;';
this.parentElement.appendChild(this.exportButton);
this.exportButton.addEventListener('click', () => this.exportNewWords());
}
exportNewWords() {
this.fetchWords('https://raw.githubusercontent.com/kuel27/wordlist/main/wordlist.txt').then(latestWords => {
const newWords = this.correctAnswers.filter(word => !latestWords.includes(word));
let blob = new Blob([newWords.join('\n')], {
type: 'text/plain;charset=utf-8'
});
let a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'newWords.txt';
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
}
fetchAndStoreLatestWordlist() {
this.fetchWords('https://raw.githubusercontent.com/kuel27/wordlist/main/wordlist.txt').then(words => {
words.forEach(word => {
if (!this.correctAnswers.includes(word)) {
this.correctAnswers.push(word);
}
});
});
}
fetchWords(url) {
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.text();
})
.then(data => data.split('\n').map(word => word.trim()))
.catch(error => {
console.error(`There was an error with the fetch operation: ${error.message}`);
return [];
});
}
observeHintsAndInput() {
this.observeHints();
this.observeInput();
this.observeChat();
}
observeHints() {
const targetNodes = [
document.querySelector('.hints .container'),
document.querySelector('.words'),
document.querySelector('#game-word'),
];
const config = {
childList: true,
subtree: true
};
const observer = new MutationObserver((mutationsList) => this.hintObserverCallback(mutationsList));
targetNodes.forEach(targetNode => {
if (targetNode) {
observer.observe(targetNode, config);
}
});
}
hintObserverCallback(mutationsList) {
const inputElem = document.querySelector('#game-chat input[data-translate="placeholder"]');
if (inputElem.value) return;
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
this.checkIfAllHintsRevealed();
this.checkWordsElement();
this.generateGuesses();
}
}
}
checkIfAllHintsRevealed() {
const hintElems = Array.from(document.querySelectorAll('.hints .hint'));
if (hintElems.every(elem => elem.classList.contains('uncover'))) {
const correctAnswer = hintElems.map(elem => elem.textContent).join('').trim().toLowerCase();
if (!correctAnswer || /[^a-zA-Z.\s-]/g.test(correctAnswer)) {
return;
}
if (!this.correctAnswers.includes(correctAnswer)) {
this.correctAnswers.push(correctAnswer);
GM_setValue('correctAnswers', this.correctAnswers);
}
}
}
observeChat() {
const chatContainer = document.querySelector('.chat-content');
const observer = new MutationObserver((mutationsList) => this.chatObserverCallback(mutationsList));
observer.observe(chatContainer, {
childList: true
});
}
chatObserverCallback(mutationsList) {
for (let mutation of mutationsList) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
let message = mutation.addedNodes[0].textContent;
let computedStyle = window.getComputedStyle(mutation.addedNodes[0]);
if (computedStyle.color === 'rgb(226, 203, 0)' && message.includes('is close!')) {
this.closeWord = message.split(' ')[0];
}
if (computedStyle.color === 'rgb(57, 117, 206)') {
this.tempWords = this.correctAnswers.slice();
this.alreadyGuessed = [];
this.closeWord = '';
}
if (message.includes(': ')) {
let guess = message.split(': ')[1];
if (!this.alreadyGuessed.includes(guess)) {
this.alreadyGuessed.push(guess);
}
}
this.generateGuesses();
}
}
}
checkWordsElement() {
const wordElems = Array.from(document.querySelectorAll('.words.show .word'));
wordElems.forEach(elem => {
const word = elem.textContent.trim().toLowerCase();
if (!word || /[^a-zA-Z.\s-]/g.test(word)) {
return;
}
if (word.trim() !== "" && !this.correctAnswers.includes(word)) {
this.correctAnswers.push(word);
GM_setValue('correctAnswers', this.correctAnswers);
}
});
}
observeInput() {
const inputElem = document.querySelector('#game-chat input[data-translate="placeholder"]');
inputElem.addEventListener('input', this.generateGuesses.bind(this));
inputElem.addEventListener('keydown', this.handleKeyDown.bind(this));
const formElem = document.querySelector('#game-chat form');
formElem.addEventListener('submit', this.generateGuesses.bind(this));
}
handleKeyDown(event) {
if (event.key === 'Tab' && this.possibleWords.length > 0) {
event.preventDefault();
const inputElem = document.querySelector('#game-chat input[data-translate="placeholder"]');
inputElem.value = this.possibleWords[0];
inputElem.focus();
this.generateGuesses();
}
}
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++) {
if (b.charAt(i - 1) == a.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1,
Math.min(matrix[i][j - 1] + 1,
matrix[i - 1][j] + 1));
}
}
}
return matrix[b.length][a.length];
}
generateGuesses() {
const hintElems = Array.from(document.querySelectorAll('.hints .hint'));
const inputElem = document.querySelector('#game-chat input[data-translate="placeholder"]');
const hintParts = hintElems.map(elem => elem.textContent === '_' ? '.' : elem.textContent).join('').split(' ');
const inputText = inputElem.value ? String(inputElem.value) : '';
this.tempWords = this.tempWords.filter(word => {
if (this.alreadyGuessed.includes(word)) {
return false;
}
if (this.closeWord.length > 0 && this.levenshteinDistance(word, this.closeWord) > 1) {
this.closeWord = '';
return false;
}
let wordParts = word.split(' ');
if (wordParts.length !== hintParts.length) {
return false;
}
for (let i = 0, len = wordParts.length; i < len; i++) {
if (wordParts[i].length !== hintParts[i].length) {
return false;
}
}
if (hintParts.join(' ').trim().length <= 0 && inputText.trim().length <= 0) {
return true;
}
let hintRegex = new RegExp(`^${hintParts.join(' ')}$`, 'i');
if (!hintRegex.test(word)) {
return false;
}
return true;
});
this.possibleWords = this.tempWords.filter(word => {
let inputTextRegex = new RegExp(`^${inputText}`, 'i');
if (!inputTextRegex.test(word)) {
return false;
}
return true;
});
this.guessElem.innerHTML = '';
this.renderGuesses(this.possibleWords, inputElem);
}
renderGuesses(possibleWords, inputElem) {
possibleWords.slice(0, 100).forEach((word, index) => {
const wordElem = document.createElement('div');
wordElem.textContent = word;
wordElem.style = 'font-weight: bold; display: inline-block; padding: 5px; margin-right: 2px; color: white; text-shadow: 2px 2px 2px black;';
const maxValue = possibleWords.length > 100 ? 100 : possibleWords.length;
let hueValue = possibleWords.length > 1 ? Math.floor(360 * index / (maxValue - 1)) : 0;
wordElem.style.backgroundColor = `hsl(${hueValue}, 100%, 50%)`;
this.addHoverEffect(wordElem, hueValue);
this.addClickFunctionality(wordElem, word, inputElem, hueValue);
this.guessElem.appendChild(wordElem);
});
}
addHoverEffect(wordElem, hueValue) {
wordElem.addEventListener('mouseenter', function() {
if (!this.classList.contains('pressed')) {
this.style.backgroundColor = 'lightgray';
}
this.classList.add('hovered');
});
wordElem.addEventListener('mouseleave', function() {
if (!this.classList.contains('pressed')) {
this.style.backgroundColor = `hsl(${hueValue}, 100%, 50%)`;
}
this.classList.remove('hovered');
});
}
addClickFunctionality(wordElem, word, inputElem, colorValue) {
wordElem.addEventListener('mousedown', function() {
wordElem.classList.add('pressed');
wordElem.style.backgroundColor = 'gray';
});
wordElem.addEventListener('mouseup', function() {
wordElem.classList.remove('pressed');
if (!wordElem.classList.contains('hovered')) {
wordElem.style.backgroundColor = `rgb(${colorValue}, ${255 - colorValue}, 0)`;
} else {
wordElem.style.backgroundColor = 'lightgray';
}
});
wordElem.addEventListener('click', function() {
const formElem = document.querySelector('#game-chat form');
inputElem.value = word;
formElem.dispatchEvent(new Event('submit', {
bubbles: true,
cancelable: true
}));
});
}
}
new WordSleuth();
})();