Skribbl 自动猜词器

一个帮助你在skribblio中猜词的脚本。

目前为 2024-08-13 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Skribbl Autoguesser
  3. // @name:zh-CN Skribbl 自动猜词器
  4. // @name:zh-TW Skribbl 自動猜詞器
  5. // @name:hi स्क्रिब्ल ऑटोगेसर
  6. // @name:es Skribbl Adivinador Automático
  7. // @namespace http://tampermonkey.net/
  8. // @supportURL https://github.com/zkisaboss/reorderedwordlist
  9. // @version 1.0
  10. // @description A script that helps you guess words in skribblio.
  11. // @description:zh-CN 一个帮助你在skribblio中猜词的脚本。
  12. // @description:zh-TW 一個幫助你在skribblio中猜詞的腳本。
  13. // @description:hi एक स्क्रिप्ट जो आपको स्क्रिब्लियो में शब्दों का अनुमान लगाने में मदद करता है।
  14. // @description:es Un script que te ayuda a adivinar palabras en skribblio.
  15. // @author Zach Kosove
  16. // @match http*://skribbl.io/*
  17. // @icon https://www.google.com/s2/favicons?sz=64&domain=skribbl.io
  18. // @grant GM_setValue
  19. // @grant GM_getValue
  20. // @license MIT
  21. // @compatible chrome
  22. // @compatible firefox
  23. // @compatible opera
  24. // @compatible safari
  25. // @compatible edge
  26. // ==/UserScript==
  27.  
  28. (function() {
  29. 'use strict';
  30.  
  31. // Variables
  32. let autoGuessing = false;
  33.  
  34.  
  35. // UI Elements
  36. const parentElement = document.createElement('div');
  37. Object.assign(parentElement.style, { position: 'fixed', bottom: '0', right: '0', width: '100%', height: 'auto' });
  38. document.body.appendChild(parentElement);
  39.  
  40. const guessElem = document.createElement('div');
  41. Object.assign(guessElem.style, { padding: '10px', backgroundColor: 'white', maxHeight: '200px', overflowX: 'auto', whiteSpace: 'nowrap', width: '100%' });
  42. parentElement.appendChild(guessElem);
  43.  
  44. const settingsElem = document.createElement('div');
  45. Object.assign(settingsElem.style, { position: 'absolute', bottom: 'calc(100%)', right: '0', padding: '10px 5px', display: 'flex', alignItems: 'center', gap: '10px' });
  46. parentElement.appendChild(settingsElem);
  47.  
  48. const autoGuessButton = document.createElement('button');
  49. autoGuessButton.innerHTML = `Auto Guess: ${autoGuessing ? 'ON' : 'OFF'}`;
  50. Object.assign(autoGuessButton.style, { padding: '5px 10px', fontSize: '12px', backgroundColor: '#333', color: '#fff' });
  51. settingsElem.appendChild(autoGuessButton);
  52.  
  53. const exportButton = document.createElement('button');
  54. exportButton.innerHTML = 'Export Answers';
  55. Object.assign(exportButton.style, { padding: '5px 10px', fontSize: '12px', backgroundColor: '#333', color: '#fff' });
  56. settingsElem.appendChild(exportButton);
  57.  
  58.  
  59. // Functions
  60. const correctAnswers = GM_getValue('correctAnswers', []);
  61.  
  62. async function fetchWords(url) {
  63. const response = await fetch(url);
  64. if (!response.ok) return [];
  65. const data = await response.text();
  66. return data.split('\n');
  67. }
  68.  
  69. async function fetchAndStoreLatestWordlist() {
  70. const words = await fetchWords('https://raw.githubusercontent.com/zkisaboss/reorderedwordlist/main/wordlist.txt');
  71.  
  72. words.forEach(word => {
  73. if (!correctAnswers.includes(word)) correctAnswers.push(word);
  74. });
  75. }
  76.  
  77. fetchAndStoreLatestWordlist();
  78.  
  79.  
  80. let myUsername = '';
  81.  
  82. function setUsername() {
  83. document.querySelectorAll(".player .player-name").forEach(playerNameElem => {
  84. const playerName = playerNameElem.textContent;
  85. if (playerName.endsWith(" (You)")) {
  86. myUsername = playerName.replace(" (You)", "");
  87. }
  88. });
  89. }
  90.  
  91. function observePlayers() {
  92. const playersContainer = document.querySelector(".players-list");
  93. if (playersContainer) {
  94. const observer = new MutationObserver(() => {
  95. if (myUsername === '') {
  96. setUsername();
  97. } else {
  98. observer.disconnect();
  99. }
  100. });
  101. observer.observe(playersContainer, { childList: true, subtree: true });
  102. }
  103. }
  104.  
  105. observePlayers();
  106.  
  107.  
  108. function observeDrawingTurn() {
  109. const wordsElement = document.querySelector('.words');
  110.  
  111. const processNewChildNodes = (childNodes) => {
  112. childNodes.forEach(childNode => {
  113. const textContent = childNode.textContent.toLowerCase().trim();
  114. if (textContent && !correctAnswers.includes(textContent)) {
  115. correctAnswers.push(textContent);
  116. GM_setValue('correctAnswers', correctAnswers);
  117. }
  118. });
  119. };
  120.  
  121. const mutationCallback = (mutationsList) => {
  122. const mutation = mutationsList.find(mutation => mutation.type === 'childList');
  123. if (mutation) {
  124. processNewChildNodes(mutation.target.childNodes);
  125. }
  126. };
  127.  
  128. const observer = new MutationObserver(mutationCallback);
  129.  
  130. observer.observe(wordsElement, { subtree: true, childList: true });
  131. }
  132.  
  133. observeDrawingTurn();
  134.  
  135.  
  136. // Core functionality
  137. let possibleWords = [];
  138.  
  139. function renderGuesses(possibleWords) {
  140. guessElem.innerHTML = '';
  141.  
  142. possibleWords.forEach((word, index) => {
  143. const wordElem = document.createElement('div');
  144. wordElem.innerHTML = word;
  145. Object.assign(wordElem.style, {
  146. fontWeight: 'bold',
  147. display: 'inline-block',
  148. padding: '5px',
  149. marginRight: '2px',
  150. color: 'white',
  151. textShadow: '2px 2px 2px black',
  152. backgroundColor: 'hsl(205, 100%, 50%)'
  153. });
  154.  
  155. wordElem.addEventListener('mouseenter', () => {
  156. if (!wordElem.classList.contains('pressed')) wordElem.style.backgroundColor = 'lightgray';
  157. wordElem.classList.add('hovered');
  158. });
  159.  
  160. wordElem.addEventListener('mouseleave', () => {
  161. if (!wordElem.classList.contains('pressed')) wordElem.style.backgroundColor = 'hsl(205, 100%, 50%)';
  162. wordElem.classList.remove('hovered');
  163. });
  164.  
  165. wordElem.addEventListener('mousedown', () => {
  166. wordElem.classList.add('pressed');
  167. wordElem.style.backgroundColor = 'gray';
  168. });
  169.  
  170. wordElem.addEventListener('mouseup', () => {
  171. wordElem.classList.remove('pressed');
  172. wordElem.style.backgroundColor = wordElem.classList.contains('hovered') ? 'lightgray' : 'hsl(205, 100%, 50%)';
  173. });
  174.  
  175. wordElem.addEventListener('click', () => {
  176. document.querySelector('#game-chat input[data-translate="placeholder"]').value = word;
  177. document.querySelector('#game-chat form').dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
  178. });
  179.  
  180. guessElem.appendChild(wordElem);
  181. });
  182. }
  183.  
  184. function generateGuesses() {
  185. const inputElem = document.querySelector('#game-chat input[data-translate="placeholder"]');
  186. const pattern = inputElem.value.toLowerCase().trim();
  187. const filteredWords = possibleWords.filter(word => word.startsWith(pattern));
  188.  
  189. if (possibleWords.length === 1) {
  190. inputElem.value = possibleWords.shift();
  191. inputElem.closest('form').dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
  192. }
  193.  
  194. renderGuesses(filteredWords);
  195. }
  196.  
  197. function filterHints(inputWords) {
  198. const hints = Array.from(document.querySelectorAll('.hints .hint'));
  199. const hintPattern = hints.map(hint => hint.textContent === '_' ? '[a-z]' : hint.textContent).join('');
  200. const hintRegex = new RegExp(`^${hintPattern}$`, 'i');
  201. return inputWords.filter(word => hintRegex.test(word));
  202. }
  203.  
  204. function observeHints() {
  205. const hintTargets = [
  206. document.querySelector('.hints .container'),
  207. document.querySelector('.words'),
  208. document.querySelector('#game-word')
  209. ].filter(Boolean);
  210.  
  211. const observer = new MutationObserver(() => {
  212. const hintElems = Array.from(document.querySelectorAll('.hints .hint'));
  213. const allUncovered = hintElems.every(elem => elem.classList.contains('uncover'));
  214.  
  215. if (allUncovered) {
  216. const correctAnswer = hintElems.map(elem => elem.textContent).join('').trim().toLowerCase();
  217. const x = 1;
  218.  
  219. if (correctAnswers.includes(correctAnswer)) {
  220. const currentIndex = correctAnswers.indexOf(correctAnswer);
  221. const newIndex = Math.max(0, currentIndex - x);
  222. correctAnswers.splice(currentIndex, 1);
  223. correctAnswers.splice(newIndex, 0, correctAnswer);
  224. } else {
  225. correctAnswers.push(correctAnswer);
  226. }
  227.  
  228. GM_setValue('correctAnswers', correctAnswers);
  229. possibleWords = [];
  230. } else {
  231. possibleWords = filterHints(possibleWords);
  232. }
  233.  
  234. generateGuesses();
  235. });
  236. hintTargets.forEach(target => observer.observe(target, { childList: true, subtree: true }));
  237. }
  238.  
  239. observeHints();
  240.  
  241.  
  242. // https://youtu.be/Dd_NgYVOdLk
  243. function levenshteinDistance(a, b) {
  244. const matrix = [];
  245. for (let i = 0; i <= b.length; i++) matrix[i] = [i];
  246. for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
  247.  
  248. for (let i = 1; i <= b.length; i++) {
  249. for (let j = 1; j <= a.length; j++) {
  250. if (b.charAt(i - 1) === a.charAt(j - 1)) {
  251. matrix[i][j] = matrix[i - 1][j - 1];
  252. } else {
  253. matrix[i][j] = Math.min(
  254. matrix[i - 1][j - 1] + 1,
  255. matrix[i][j - 1] + 1,
  256. matrix[i - 1][j] + 1
  257. );
  258. }
  259. }
  260. }
  261. return matrix[b.length][a.length];
  262. }
  263.  
  264. let previousWords = [];
  265.  
  266. function handleChatMessage(messageNode) {
  267. const messageColor = window.getComputedStyle(messageNode).color;
  268. const message = messageNode.textContent;
  269.  
  270. if (messageColor === 'rgb(57, 117, 206)') {
  271. possibleWords = filterHints(correctAnswers);
  272.  
  273. generateGuesses();
  274.  
  275. } else if (message.includes(': ')) {
  276. const [username, guess] = message.split(': ');
  277. possibleWords = possibleWords.filter(word => word !== guess);
  278. previousWords = possibleWords;
  279.  
  280. if (username === myUsername) {
  281. possibleWords = possibleWords.filter(word => levenshteinDistance(word, guess) > 1);
  282. }
  283.  
  284. generateGuesses();
  285.  
  286. } else if (messageColor === 'rgb(226, 203, 0)' && message.endsWith('is close!')) {
  287. const closeWord = message.replace(' is close!', ''); // works for multi-word guesses?
  288. possibleWords = previousWords.filter(word => levenshteinDistance(word, closeWord) === 1);
  289.  
  290. generateGuesses();
  291. }
  292. }
  293.  
  294. function observeChat() {
  295. const chatContainer = document.querySelector('.chat-content');
  296. if (chatContainer) {
  297. const observer = new MutationObserver(mutationsList => {
  298. mutationsList.forEach(mutation => {
  299. if (mutation.addedNodes.length > 0) handleChatMessage(mutation.addedNodes[0]);
  300. });
  301. });
  302. observer.observe(chatContainer, { childList: true });
  303. }
  304. }
  305.  
  306. observeChat();
  307.  
  308.  
  309. function observeInput() {
  310. const inputElem = document.querySelector('#game-chat input[data-translate="placeholder"]');
  311.  
  312. inputElem.addEventListener('input', generateGuesses);
  313.  
  314. inputElem.addEventListener('keydown', ({ key }) => {
  315. if (key === 'Enter') {
  316. inputElem.value = guessElem.querySelector('div').innerText;
  317. inputElem.closest('form').dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
  318. }
  319. });
  320. }
  321.  
  322. observeInput();
  323.  
  324.  
  325. let autoGuessInterval;
  326.  
  327. function startAutoGuessing() {
  328. if (autoGuessing) {
  329. autoGuessInterval = setInterval(() => {
  330. if (possibleWords.length > 0) {
  331. document.querySelector('#game-chat input[data-translate="placeholder"]').value = possibleWords.shift();
  332. document.querySelector('#game-chat form').dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
  333. }
  334. }, 10000);
  335. }
  336. }
  337.  
  338. startAutoGuessing();
  339.  
  340.  
  341. function toggleAutoGuessing() {
  342. autoGuessing = !autoGuessing;
  343. autoGuessButton.innerHTML = `Auto Guess: ${autoGuessing ? 'ON' : 'OFF'}`;
  344.  
  345. if (autoGuessing) {
  346. startAutoGuessing();
  347. } else {
  348. clearInterval(autoGuessInterval);
  349. autoGuessInterval = null;
  350. }
  351. }
  352.  
  353. autoGuessButton.addEventListener('click', toggleAutoGuessing);
  354.  
  355.  
  356. async function exportNewWords() {
  357. const old = await fetchWords('https://raw.githubusercontent.com/zkisaboss/reorderedwordlist/main/wordlist.txt');
  358. const newWords = correctAnswers.filter(word => !old.includes(word));
  359.  
  360. const blob = new Blob([newWords.join('\n')], { type: 'text/plain;charset=utf-8' });
  361.  
  362. const anchor = document.createElement('a');
  363. anchor.href = URL.createObjectURL(blob);
  364. anchor.download = 'newWords.txt';
  365. // anchor.style.display = 'none';
  366.  
  367. document.body.appendChild(anchor);
  368. anchor.click();
  369.  
  370. document.body.removeChild(anchor);
  371. }
  372.  
  373.  
  374. exportButton.addEventListener('click', exportNewWords);
  375. })();