Skribbl 自动猜词器

自动在 Skribbl.io 中猜词,快速、简单、有效。

  1. // ==UserScript==
  2. // @name Skribbl AutoGuesser
  3. // @name:zh-CN Skribbl 自动猜词器
  4. // @name:zh-TW Skribbl 自動猜詞器
  5. // @name:hi Skribbl स्वतः अनुमान स्क्रिप्ट
  6. // @name:es Skribbl Adivinador Automático
  7. // @namespace http://tampermonkey.net/
  8. // @version 1.08
  9. // @description Automatically suggests guesses in Skribbl.io. Fast, easy, and effective.
  10. // @description:zh-CN 自动在 Skribbl.io 中猜词,快速、简单、有效。
  11. // @description:zh-TW 自動在 Skribbl.io 中猜詞,快速、簡單、有效。
  12. // @description:hi Skribbl.io में शब्दों का अनुमान लगाने वाली तेज़ और आसान स्क्रिप्ट।
  13. // @description:es Adivina palabras automáticamente en Skribbl.io de forma rápida y sencilla.
  14. // @author Zach Kosove
  15. // @supportURL https://github.com/zkisaboss/reorderedwordlist
  16. // @match https://skribbl.io/*
  17. // @icon https://skribbl.io/favicon.png
  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. function createUI() {
  32. const bottomUI = document.createElement('div');
  33. bottomUI.id = 'bottom-ui';
  34. bottomUI.innerHTML = `
  35. <div id="settings-shelf" class="section">
  36. <button class="ui-btn" id="remaining-guesses">Remaining Guesses: 0</button>
  37. <button class="ui-btn" id="auto-guess">Auto Guess: OFF</button>
  38. <button class="ui-btn" id="export-answers">Export Answers</button>
  39. <button class="ui-btn ui-btn-secondary" id="get-special">Secret</button>
  40. </div>
  41. <div id="guess-shelf" class="section"></div>
  42. <style>
  43. #bottom-ui {
  44. position: fixed;
  45. bottom: 0;
  46. left: 0;
  47. width: 100%;
  48. background:
  49. linear-gradient(135deg, rgba(255, 255, 255, 0.3), rgba(200, 200, 255, 0.15));
  50. backdrop-filter: blur(30px) saturate(180%);
  51. -webkit-backdrop-filter: blur(30px) saturate(180%);
  52. border-top-left-radius: 24px;
  53. border-top-right-radius: 24px;
  54. border-top: 1px solid rgba(255, 255, 255, 0.25);
  55. border-left: 1px solid rgba(255, 255, 255, 0.1);
  56. border-right: 1px solid rgba(255, 255, 255, 0.1);
  57. box-shadow:
  58. 0 -12px 30px rgba(0, 0, 0, 0.15),
  59. inset 0 1px 0 rgba(255, 255, 255, 0.5);
  60. display: flex;
  61. flex-direction: column;
  62. z-index: 1000;
  63. overflow: hidden;
  64. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  65. transition: transform 0.15s cubic-bezier(0.4, 0, 1, 1);
  66. }
  67.  
  68. .hidden {
  69. transform: translateY(100%);
  70. transition: transform 0.15s cubic-bezier(0.4, 0, 1, 1);
  71. }
  72.  
  73. .section {
  74. display: flex;
  75. gap: 12px;
  76. padding: 14px 24px;
  77. overflow-x: auto;
  78. white-space: nowrap;
  79. -webkit-overflow-scrolling: touch;
  80. }
  81.  
  82. .section::-webkit-scrollbar {
  83. height: 6px;
  84. }
  85.  
  86. .section::-webkit-scrollbar-thumb {
  87. background: rgba(255, 255, 255, 0.2);
  88. border-radius: 3px;
  89. }
  90.  
  91. .ui-btn {
  92. flex: 0 0 auto;
  93. font-size: 15px;
  94. font-weight: 500;
  95. letter-spacing: 0.25px;
  96. padding: 10px 18px;
  97. border: 0.5px solid rgba(255, 255, 255, 0.25);
  98. border-radius: 14px;
  99. background: linear-gradient(135deg, rgba(120, 120, 255, 0.5), rgba(100, 100, 230, 0.35));
  100. color: #ffffff;
  101. cursor: pointer;
  102. box-shadow:
  103. 0 0 10px rgba(120, 120, 255, 0.5),
  104. inset 0 0 2px rgba(255, 255, 255, 0.2),
  105. 0 1px 2px rgba(0, 0, 0, 0.15);
  106. transition: background 0.3s ease, transform 0.2s ease, box-shadow 0.3s ease;
  107. backdrop-filter: blur(8px);
  108. -webkit-backdrop-filter: blur(8px);
  109. text-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
  110. }
  111.  
  112. .ui-btn:hover {
  113. background: linear-gradient(135deg, rgba(140, 140, 255, 0.7), rgba(120, 120, 255, 0.6));
  114. box-shadow:
  115. 0 0 18px rgba(140, 140, 255, 0.75),
  116. inset 0 0 3px rgba(255, 255, 255, 0.3);
  117. transform: scale(1.04);
  118. }
  119.  
  120. .ui-btn:active {
  121. transform: scale(0.97);
  122. box-shadow:
  123. 0 0 8px rgba(100, 100, 200, 0.4),
  124. inset 0 0 6px rgba(255, 255, 255, 0.2);
  125. }
  126.  
  127. .ui-btn-secondary {
  128. display: inline-block;
  129. background: linear-gradient(135deg, rgba(255, 120, 255, 0.5), rgba(200, 100, 230, 0.35));
  130. color: #ffeaff;
  131. border: 0.5px solid rgba(255, 180, 255, 0.3);
  132. }
  133.  
  134. .ui-btn-secondary:hover {
  135. background: linear-gradient(135deg, rgba(255, 140, 255, 0.7), rgba(220, 120, 240, 0.6));
  136. box-shadow:
  137. 0 0 18px rgba(255, 140, 255, 0.75),
  138. inset 0 0 3px rgba(255, 255, 255, 0.3);
  139. transform: scale(1.04);
  140. }
  141. </style>
  142. `;
  143. document.body.appendChild(bottomUI);
  144.  
  145. const ui = document.getElementById('bottom-ui');
  146. document.addEventListener('keydown', (e) => {
  147. if (e.key === 'ArrowDown') ui.classList.add('hidden');
  148. if (e.key === 'ArrowUp') ui.classList.remove('hidden');
  149. });
  150.  
  151. }
  152.  
  153. createUI();
  154.  
  155.  
  156. const correctAnswers = GM_getValue('correctAnswers', []);
  157.  
  158. async function fetchWords(url) {
  159. const response = await fetch(url);
  160. if (!response.ok) return [];
  161.  
  162. const text = await response.text();
  163. return text.split('\n').filter(word => word !== '');
  164. }
  165.  
  166. async function fetchAndStoreLatestWordlist() {
  167. const words = await fetchWords('https://raw.githubusercontent.com/zkisaboss/reorderedwordlist/main/wordlist_test.txt');
  168.  
  169. words.forEach(word => {
  170. if (!correctAnswers.includes(word)) correctAnswers.push(word);
  171. });
  172. }
  173.  
  174. fetchAndStoreLatestWordlist();
  175.  
  176.  
  177. let myUsername = '';
  178.  
  179. function findUsername() {
  180. const target = document.querySelector(".players-list");
  181. if (!target) return;
  182.  
  183. const observer = new MutationObserver(() => {
  184. myUsername = document.querySelector(".me").textContent.replace(" (You)", "")
  185. observer.disconnect();
  186. });
  187.  
  188. observer.observe(target, { childList: true });
  189. }
  190.  
  191. findUsername();
  192.  
  193.  
  194. function observeDrawingTurn() {
  195. const target = document.querySelector('.words');
  196. if (!target) return;
  197.  
  198. const observer = new MutationObserver(() => {
  199. target.childNodes.forEach(word => {
  200. const text = word.textContent.toLowerCase();
  201.  
  202. if (!correctAnswers.includes(text)) {
  203. correctAnswers.push(text);
  204. GM_setValue('correctAnswers', correctAnswers);
  205. }
  206. });
  207. });
  208.  
  209. observer.observe(target, { childList: true });
  210. }
  211.  
  212. observeDrawingTurn();
  213.  
  214.  
  215. const remainingButton = document.getElementById('remaining-guesses');
  216.  
  217. const guessShelf = document.getElementById('guess-shelf');
  218.  
  219. let possibleWords = [];
  220.  
  221. const input = document.querySelector('#game-chat input[data-translate="placeholder"]');
  222.  
  223. function renderGuesses(words) {
  224. guessShelf.innerHTML = '';
  225. remainingButton.textContent = `Remaining Guesses: ${possibleWords.length}`;
  226.  
  227. words.forEach(word => {
  228. const button = Object.assign(document.createElement('button'), {
  229. className: 'ui-btn',
  230. textContent: word,
  231. onclick: () => {
  232. input.value = word;
  233. input.closest('form').dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
  234. }
  235. });
  236. guessShelf.appendChild(button);
  237. });
  238. };
  239.  
  240. function generateGuesses() {
  241. if (possibleWords.length === 1) {
  242. input.value = possibleWords.shift();
  243. input.closest('form').dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
  244. }
  245.  
  246. const pattern = input.value.toLowerCase().trim();
  247. const words = possibleWords.filter(word => word.startsWith(pattern));
  248. renderGuesses(words);
  249. }
  250.  
  251. function observeInput() {
  252. input.addEventListener('input', generateGuesses);
  253.  
  254. input.addEventListener('keydown', ({ key }) => {
  255. if (key === 'Enter') {
  256. input.value = guessShelf.firstElementChild.innerText;
  257. input.closest('form').dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
  258. }
  259. });
  260. }
  261.  
  262. observeInput();
  263.  
  264.  
  265. function storeAnswer(word) {
  266. if (correctAnswers.includes(word)) {
  267. const index = correctAnswers.indexOf(word);
  268. const newIndex = Math.max(0, index - 1);
  269. correctAnswers.splice(index, 1);
  270. correctAnswers.splice(newIndex, 0, word);
  271. }
  272. else {
  273. correctAnswers.push(word);
  274. }
  275.  
  276. GM_setValue('correctAnswers', correctAnswers);
  277. return [];
  278. }
  279.  
  280. function filterHints(inputWords) {
  281. const hints = Array.from(document.querySelectorAll('.hints .hint'));
  282. const combined = hints.map(hint => hint.textContent === '_' ? '[a-z]' : hint.textContent).join('').toLowerCase();
  283.  
  284. const allUncovered = hints.every(hint => hint.classList.contains('uncover'));
  285. if (allUncovered) return storeAnswer(combined);
  286.  
  287. const regex = new RegExp(`^${combined}$`, 'i');
  288. return inputWords.filter(word => regex.test(word));
  289. }
  290.  
  291. function observeHints() {
  292. const target = document.querySelector('.hints .container');
  293. if (!target) return;
  294.  
  295. const observer = new MutationObserver(() => {
  296. possibleWords = filterHints(possibleWords);
  297. generateGuesses();
  298. });
  299.  
  300. observer.observe(target, { childList: true, subtree: true });
  301. }
  302.  
  303. observeHints();
  304.  
  305.  
  306. /*
  307. // Levenshtein: O(n * m) time, O(n * m) space
  308. // https://youtu.be/Dd_NgYVOdLk
  309. function levenshteinDistance(a, b) {
  310. const matrix = [];
  311. for (let i = 0; i <= b.length; i++) matrix[i] = [i];
  312. for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
  313.  
  314. for (let i = 1; i <= b.length; i++) {
  315. for (let j = 1; j <= a.length; j++) {
  316. matrix[i][j] = Math.min(
  317. matrix[i - 1][j] + 1,
  318. matrix[i][j - 1] + 1,
  319. matrix[i - 1][j - 1] + (b[i - 1] !== a[j - 1])
  320. );
  321. }
  322. }
  323. return matrix[b.length][a.length];
  324. }
  325. */
  326.  
  327. // Banded Levenshtein: O(min(|n|, |m|) * k) time, O(min(m,n)) space
  328. // https://www.baeldung.com/cs/levenshtein-distance-computation
  329. function levenshteinDistance(a, b, k = 1) {
  330. if (a.length > b.length) {
  331. const t = a;
  332. a = b;
  333. b = t;
  334. }
  335. const m = a.length;
  336. const n = b.length;
  337.  
  338. if (n - m > k) return -1;
  339. if (m === 0) return n;
  340.  
  341. const rows = [ new Uint16Array(m + 1), new Uint16Array(m + 1) ];
  342.  
  343. for (let j = 0; j <= m; j++) rows[0][j] = j;
  344.  
  345. let flip = 0;
  346. for (let i = 0; i < n; i++) {
  347. flip ^= 1;
  348. const curr = rows[flip];
  349. const prev = rows[flip ^ 1];
  350.  
  351. curr[0] = i + 1;
  352.  
  353. const stripeStart = Math.max(1, i + 1 - k);
  354. const stripeEnd = Math.min(m, i + 1 + k);
  355.  
  356. let rowMin = k + 1;
  357. for (let j = stripeStart; j <= stripeEnd; j++) {
  358. curr[j] = Math.min(
  359. prev[j] + 1,
  360. curr[j - 1] + 1,
  361. prev[j - 1] + (a[j - 1] !== b[i])
  362. );
  363.  
  364. if (curr[j] < rowMin) rowMin = curr[j];
  365. }
  366. if (rowMin > k) return -1;
  367. }
  368. return rows[flip][m];
  369. }
  370.  
  371.  
  372. let previousWords = [];
  373.  
  374. function handleChatMessage(messageNode) {
  375. const messageColor = window.getComputedStyle(messageNode).color;
  376. const message = messageNode.textContent;
  377.  
  378. if (messageColor === 'rgb(57, 117, 206)' && message.endsWith('is drawing now!')) {
  379. possibleWords = filterHints(correctAnswers);
  380.  
  381. generateGuesses();
  382. }
  383.  
  384. else if (message.includes(': ')) {
  385. const [username, guess] = message.split(': ');
  386. possibleWords = possibleWords.filter(word => word !== guess);
  387. previousWords = possibleWords;
  388.  
  389. if (username === myUsername) {
  390. possibleWords = possibleWords.filter(word => levenshteinDistance(word, guess) === -1);
  391. }
  392.  
  393. generateGuesses();
  394. }
  395.  
  396. else if (messageColor === 'rgb(226, 203, 0)' && message.endsWith('is close!')) {
  397. const closeWord = message.replace(' is close!', '');
  398. possibleWords = previousWords.filter(word => levenshteinDistance(word, closeWord) === 1);
  399.  
  400. generateGuesses();
  401. }
  402. }
  403.  
  404. function observeChat() {
  405. const target = document.querySelector('.chat-content');
  406. if (!target) return;
  407.  
  408. const observer = new MutationObserver(() => {
  409. const lastMessage = target.lastElementChild;
  410. if (lastMessage) handleChatMessage(lastMessage);
  411. });
  412.  
  413. observer.observe(target, { childList: true });
  414. }
  415.  
  416. observeChat();
  417.  
  418.  
  419. let autoGuessInterval;
  420.  
  421. let autoGuessing = false;
  422.  
  423. function startAutoGuessing() {
  424. if (!autoGuessing) return;
  425.  
  426. autoGuessInterval = setInterval(() => {
  427. if (possibleWords.length > 0) {
  428. input.value = possibleWords.shift();
  429. input.closest('form').dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
  430. }
  431. }, 7500);
  432. }
  433.  
  434. startAutoGuessing();
  435.  
  436.  
  437. const autoGuessButton = document.getElementById('auto-guess');
  438.  
  439. function toggleAutoGuessing() {
  440. autoGuessing = !autoGuessing;
  441. autoGuessButton.innerHTML = `Auto Guess: ${autoGuessing ? 'ON' : 'OFF'}`;
  442.  
  443. if (autoGuessing) {
  444. startAutoGuessing();
  445. } else {
  446. clearInterval(autoGuessInterval);
  447. autoGuessInterval = null;
  448. }
  449. }
  450.  
  451. autoGuessButton.addEventListener('click', toggleAutoGuessing);
  452.  
  453.  
  454. async function exportNewWords() {
  455. const old = await fetchWords('https://raw.githubusercontent.com/zkisaboss/reorderedwordlist/main/wordlist.txt');
  456. const newWords = correctAnswers.filter(word => !old.includes(word));
  457.  
  458. const blob = new Blob([newWords.join('\n')], { type: 'text/plain;charset=utf-8' });
  459.  
  460. const anchor = document.createElement('a');
  461. anchor.href = URL.createObjectURL(blob);
  462. anchor.download = 'newWords.txt';
  463.  
  464. document.body.appendChild(anchor);
  465. anchor.click();
  466.  
  467. document.body.removeChild(anchor);
  468. }
  469.  
  470. const exportButton = document.getElementById('export-answers');
  471.  
  472. exportButton.addEventListener('click', exportNewWords);
  473.  
  474.  
  475. const secretButton = document.getElementById('get-special');
  476.  
  477. function runSecret() {
  478. const avatars = document.querySelectorAll('.avatar-container .avatar');
  479.  
  480. const interval = setInterval(() => {
  481. let allSecretsVisible = true;
  482.  
  483. avatars.forEach(avatar => {
  484. const secret = avatar.querySelector('.special');
  485.  
  486. if (getComputedStyle(secret).display === 'none') {
  487. avatar.click();
  488. allSecretsVisible = false;
  489. }
  490. });
  491.  
  492. if (allSecretsVisible) clearInterval(interval);
  493. }, 15);
  494. }
  495.  
  496. secretButton.addEventListener('click', runSecret);
  497.  
  498.  
  499. function observeSecret() {
  500. const target = document.getElementById('home');
  501. if (!target) return;
  502.  
  503. const observer = new MutationObserver(() => {
  504. secretButton.style.display = target.hasAttribute('style') ? 'none' : 'inline-block';
  505. });
  506.  
  507. observer.observe(target, { attributes: true, attributeFilter: ['style'] });
  508. }
  509.  
  510. observeSecret();
  511. })();