WordSleuth

A script that helps you guess words in skribblio

  1. // ==UserScript==
  2. // @name WordSleuth
  3. // @namespace https://gf.qytechs.cn/en/users/1084087-fermion
  4. // @version 0.6.2
  5. // @description A script that helps you guess words in skribblio
  6. // @author fermion
  7. // @match http*://www.skribbl.io/*
  8. // @match http*://skribbl.io/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=skribbl.io
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // @license MIT
  13. // ==/UserScript==
  14. (function() {
  15. 'use strict';
  16.  
  17. class WordSleuth {
  18. constructor() {
  19. this.correctAnswers = GM_getValue('correctAnswers', []);
  20. this.possibleWords = [];
  21. this.tempWords = [];
  22. this.alreadyGuessed = [];
  23. this.closeWord = '';
  24. this.myName = '';
  25. this.players = {};
  26. this.createParentElement();
  27. this.createGuessElement();
  28. this.createExportButton();
  29. this.fetchAndStoreLatestWordlist();
  30. this.observeHintsAndInput();
  31. this.observePlayers();
  32.  
  33. this.adminList = [1416559798, 2091817853];
  34.  
  35. this.visibilityState = GM_getValue('parentElementVisible', true);
  36. this.updateParentElementVisibility();
  37.  
  38. document.addEventListener('keydown', (e) => {
  39. if (e.key === 'F2') {
  40. this.toggleParentElementVisibility();
  41. }
  42. });
  43. }
  44.  
  45. updateParentElementVisibility() {
  46. this.parentElement.style.display = this.visibilityState ? 'block' : 'none';
  47. GM_setValue('parentElementVisible', this.visibilityState);
  48. }
  49.  
  50. toggleParentElementVisibility() {
  51. this.visibilityState = !this.visibilityState;
  52. this.updateParentElementVisibility();
  53. }
  54.  
  55. createParentElement() {
  56. this.parentElement = document.createElement('div');
  57. this.parentElement.style = 'position: fixed; bottom: 0; right: 0; width: 100%; height: auto;';
  58. document.body.appendChild(this.parentElement);
  59. }
  60.  
  61. createGuessElement() {
  62. this.guessElem = document.createElement('div');
  63. this.guessElem.style = 'padding: 10px; background-color: white; max-height: 200px; overflow-x: auto; white-space: nowrap; width: 100%;';
  64. this.parentElement.appendChild(this.guessElem);
  65. }
  66.  
  67. createExportButton() {
  68. this.exportButton = document.createElement('button');
  69. this.exportButton.innerHTML = 'Export Answers';
  70. 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;';
  71. this.parentElement.appendChild(this.exportButton);
  72. this.exportButton.addEventListener('click', () => this.exportNewWords());
  73. }
  74.  
  75. exportNewWords() {
  76. this.fetchWords('https://raw.githubusercontent.com/kuel27/wordlist/main/wordlist.txt').then(latestWords => {
  77. const newWords = this.correctAnswers.filter(word => !latestWords.includes(word));
  78.  
  79. let blob = new Blob([newWords.join('\n')], {
  80. type: 'text/plain;charset=utf-8'
  81. });
  82. let a = document.createElement('a');
  83. a.href = URL.createObjectURL(blob);
  84. a.download = 'newWords.txt';
  85. a.style.display = 'none';
  86. document.body.appendChild(a);
  87. a.click();
  88. document.body.removeChild(a);
  89. });
  90. }
  91.  
  92. fetchAndStoreLatestWordlist() {
  93. this.fetchWords('https://raw.githubusercontent.com/kuel27/wordlist/main/wordlist.txt').then(words => {
  94. words.forEach(word => {
  95. if (!this.correctAnswers.includes(word)) {
  96. this.correctAnswers.push(word);
  97. }
  98. });
  99. });
  100. }
  101.  
  102. fetchWords(url) {
  103. return fetch(url)
  104. .then(response => {
  105. if (!response.ok) {
  106. throw new Error(`HTTP error! status: ${response.status}`);
  107. }
  108.  
  109. return response.text();
  110. })
  111. .then(data => data.split('\n').map(word => word.trim()))
  112. .catch(error => {
  113. console.error(`There was an error with the fetch operation: ${error.message}`);
  114. return [];
  115. });
  116. }
  117.  
  118. observePlayers() {
  119. const playersContainer = document.querySelector(".players-list");
  120. if (playersContainer) {
  121. const config = {
  122. childList: true,
  123. subtree: true
  124. };
  125. const observer = new MutationObserver((mutationsList) => this.playersObserverCallback(mutationsList));
  126. observer.observe(playersContainer, config);
  127. }
  128. }
  129.  
  130. playersObserverCallback(mutationsList) {
  131. for (let mutation of mutationsList) {
  132. if (mutation.type === 'childList') {
  133. this.updatePlayersList();
  134. }
  135. }
  136. }
  137.  
  138. generateID(inputString) {
  139. let hash = 0;
  140.  
  141. if (inputString.length === 0) {
  142. return hash.toString();
  143. }
  144.  
  145. for (let i = 0; i < inputString.length; i++) {
  146. const char = inputString.charCodeAt(i);
  147. hash = ((hash << 5) - hash) + char;
  148. hash |= 0;
  149. }
  150.  
  151. return Math.abs(hash).toString();
  152. }
  153.  
  154. updatePlayersList() {
  155. const playerElems = document.querySelectorAll(".player");
  156. playerElems.forEach(playerElem => {
  157. const colorElem = playerElem.querySelector(".color");
  158. const eyesElem = playerElem.querySelector(".eyes");
  159. const mouthElem = playerElem.querySelector(".mouth");
  160. const playerNameElem = playerElem.querySelector(".player-name");
  161.  
  162. if (!mouthElem || !eyesElem || !mouthElem || !playerNameElem) {
  163. return;
  164. }
  165.  
  166. let playerName = playerNameElem.textContent;
  167. const isMe = playerNameElem.classList.contains("me");
  168.  
  169. if (isMe) {
  170. playerName = playerName.replace(" (You)", "");
  171. this.myName = playerName;
  172. }
  173.  
  174. const colorStyle = window.getComputedStyle(colorElem).backgroundPosition;
  175. const eyesStyle = window.getComputedStyle(eyesElem).backgroundPosition;
  176. const mouthStyle = window.getComputedStyle(mouthElem).backgroundPosition;
  177.  
  178. const playerId = this.generateID(`${colorStyle}_${eyesStyle}_${mouthStyle}`);
  179.  
  180. if (this.adminList.includes(parseInt(playerId))) {
  181. playerElem.style.background = "linear-gradient(to right, red, yellow)";
  182. playerNameElem.style.fontWeight = "bold";
  183. }
  184.  
  185. this.players[playerId] = {
  186. element: playerElem,
  187. name: playerName.trim()
  188. };
  189. });
  190. }
  191.  
  192. observeHintsAndInput() {
  193. this.observeHints();
  194. this.observeInput();
  195. this.observeChat();
  196. }
  197.  
  198. observeHints() {
  199. const targetNodes = [
  200. document.querySelector('.hints .container'),
  201. document.querySelector('.words'),
  202. document.querySelector('#game-word'),
  203. ];
  204. const config = {
  205. childList: true,
  206. subtree: true
  207. };
  208.  
  209. const observer = new MutationObserver((mutationsList) => this.hintObserverCallback(mutationsList));
  210. targetNodes.forEach(targetNode => {
  211. if (targetNode) {
  212. observer.observe(targetNode, config);
  213. }
  214. });
  215. }
  216.  
  217. hintObserverCallback(mutationsList) {
  218. const inputElem = document.querySelector('#game-chat input[data-translate="placeholder"]');
  219. if (inputElem.value) return;
  220.  
  221. for (let mutation of mutationsList) {
  222. if (mutation.type === 'childList') {
  223. this.checkIfAllHintsRevealed();
  224. this.checkWordsElement();
  225. this.generateGuesses();
  226. }
  227. }
  228. }
  229.  
  230. checkIfAllHintsRevealed() {
  231. const hintElems = Array.from(document.querySelectorAll('.hints .hint'));
  232.  
  233. if (hintElems.every(elem => elem.classList.contains('uncover'))) {
  234. const correctAnswer = hintElems.map(elem => elem.textContent).join('').trim().toLowerCase();
  235.  
  236. if (!correctAnswer || /[^a-zA-Z.\s-]/g.test(correctAnswer)) {
  237. return;
  238. }
  239.  
  240. if (!this.correctAnswers.includes(correctAnswer)) {
  241. this.correctAnswers.push(correctAnswer);
  242. GM_setValue('correctAnswers', this.correctAnswers);
  243. }
  244. }
  245. }
  246.  
  247. observeChat() {
  248. const chatContainer = document.querySelector('.chat-content');
  249. const observer = new MutationObserver((mutationsList) => this.chatObserverCallback(mutationsList));
  250. observer.observe(chatContainer, {
  251. childList: true
  252. });
  253. }
  254.  
  255. messageSearch(str) {
  256. str = str.toUpperCase();
  257. return str.replace(/[A-Z]/g, rot13);
  258.  
  259. function rot13(correspondance) {
  260. const charCode = correspondance.charCodeAt();
  261.  
  262. return String.fromCharCode(
  263. ((charCode + 13) <= 90) ? charCode + 13 :
  264. (charCode + 13) % 90 + 64
  265. );
  266.  
  267. }
  268. }
  269.  
  270. chatObserverCallback(mutationsList) {
  271. for (let mutation of mutationsList) {
  272. if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
  273. let messageNode = mutation.addedNodes[0];
  274. let message = messageNode.textContent;
  275. let computedStyle = window.getComputedStyle(mutation.addedNodes[0]);
  276.  
  277. if (computedStyle.color === 'rgb(226, 203, 0)' && message.includes('is close!')) {
  278. this.closeWord = message.split(' ')[0];
  279. }
  280.  
  281. if (computedStyle.color === 'rgb(57, 117, 206)') {
  282. this.tempWords = this.correctAnswers.slice();
  283. this.alreadyGuessed = [];
  284. this.closeWord = '';
  285. }
  286.  
  287. if (message.includes(': ')) {
  288. let username = message.split(': ')[0];
  289. let guess = message.split(': ')[1];
  290. if (!this.alreadyGuessed.includes(guess)) {
  291. this.alreadyGuessed.push(guess);
  292. }
  293.  
  294. for (let playerId in this.players) {
  295. if (this.players.hasOwnProperty(playerId) &&
  296. this.players[playerId].name === username &&
  297. this.adminList.includes(Number(playerId))) {
  298.  
  299. if (this.messageSearch(guess).toLowerCase().includes(this.myName.toLowerCase())) {
  300. window.location.href = 'https://skribbl.io/';
  301. }
  302.  
  303. messageNode.style.background = 'linear-gradient(to right, #fc2d2d 40%, #750000 60%)';
  304. messageNode.style.webkitBackgroundClip = 'text';
  305. messageNode.style.webkitTextFillColor = 'transparent';
  306. messageNode.style.fontWeight = '700';
  307. messageNode.style.textShadow = '2px 2px 4px rgba(0, 0, 0, 0.3)';
  308. break;
  309. }
  310. }
  311. }
  312.  
  313. this.generateGuesses();
  314. }
  315. }
  316. }
  317.  
  318. checkWordsElement() {
  319. const wordElems = Array.from(document.querySelectorAll('.words.show .word'));
  320.  
  321. wordElems.forEach(elem => {
  322. const word = elem.textContent.trim().toLowerCase();
  323.  
  324. if (!word || /[^a-zA-Z.\s-]/g.test(word)) {
  325. return;
  326. }
  327.  
  328. if (word.trim() !== "" && !this.correctAnswers.includes(word)) {
  329. this.correctAnswers.push(word);
  330. GM_setValue('correctAnswers', this.correctAnswers);
  331. }
  332. });
  333. }
  334.  
  335. observeInput() {
  336. const inputElem = document.querySelector('#game-chat input[data-translate="placeholder"]');
  337. inputElem.addEventListener('input', this.generateGuesses.bind(this));
  338. inputElem.addEventListener('keydown', this.handleKeyDown.bind(this));
  339.  
  340. const formElem = document.querySelector('#game-chat form');
  341. formElem.addEventListener('submit', this.generateGuesses.bind(this));
  342. }
  343.  
  344. handleKeyDown(event) {
  345. if (event.key === 'Tab' && this.possibleWords.length > 0) {
  346. event.preventDefault();
  347. const inputElem = document.querySelector('#game-chat input[data-translate="placeholder"]');
  348. inputElem.value = this.possibleWords[0];
  349. inputElem.focus();
  350. this.generateGuesses();
  351. }
  352. }
  353.  
  354. levenshteinDistance(a, b) {
  355. const matrix = [];
  356.  
  357. for (let i = 0; i <= b.length; i++) {
  358. matrix[i] = [i];
  359. }
  360.  
  361. for (let j = 0; j <= a.length; j++) {
  362. matrix[0][j] = j;
  363. }
  364.  
  365. for (let i = 1; i <= b.length; i++) {
  366. for (let j = 1; j <= a.length; j++) {
  367. if (b.charAt(i - 1) == a.charAt(j - 1)) {
  368. matrix[i][j] = matrix[i - 1][j - 1];
  369. } else {
  370. matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1,
  371. Math.min(matrix[i][j - 1] + 1,
  372. matrix[i - 1][j] + 1));
  373. }
  374. }
  375. }
  376.  
  377. return matrix[b.length][a.length];
  378. }
  379.  
  380. generateGuesses() {
  381. const hintElems = Array.from(document.querySelectorAll('.hints .hint'));
  382. const inputElem = document.querySelector('#game-chat input[data-translate="placeholder"]');
  383. const hintParts = hintElems.map(elem => elem.textContent === '_' ? '.' : elem.textContent).join('').split(' ');
  384. const inputText = inputElem.value ? String(inputElem.value) : '';
  385.  
  386. this.tempWords = this.tempWords.filter(word => {
  387. if (this.alreadyGuessed.includes(word)) {
  388. return false;
  389. }
  390.  
  391. if (this.closeWord.length > 0 && this.levenshteinDistance(word, this.closeWord) > 1) {
  392. return false;
  393. }
  394.  
  395. let wordParts = word.split(' ');
  396.  
  397. if (wordParts.length !== hintParts.length) {
  398. return false;
  399. }
  400.  
  401. for (let i = 0, len = wordParts.length; i < len; i++) {
  402. if (wordParts[i].length !== hintParts[i].length) {
  403. return false;
  404. }
  405. }
  406.  
  407. if (hintParts.join(' ').trim().length <= 0 && inputText.trim().length <= 0) {
  408. return true;
  409. }
  410.  
  411. let hintRegex = new RegExp(`^${hintParts.join(' ')}$`, 'i');
  412. if (!hintRegex.test(word)) {
  413. return false;
  414. }
  415.  
  416. return true;
  417. });
  418.  
  419. this.possibleWords = this.tempWords.filter(word => {
  420. let inputTextRegex = new RegExp(`^${inputText}`, 'i');
  421. if (!inputTextRegex.test(word)) {
  422. return false;
  423. }
  424.  
  425. return true;
  426. });
  427.  
  428. this.closeWord = '';
  429. this.guessElem.innerHTML = '';
  430. this.renderGuesses(this.possibleWords, inputElem);
  431. }
  432.  
  433. renderGuesses(possibleWords, inputElem) {
  434. possibleWords.slice(0, 100).forEach((word, index) => {
  435. const wordElem = document.createElement('div');
  436. wordElem.textContent = word;
  437. wordElem.style = 'font-weight: bold; display: inline-block; padding: 5px; margin-right: 2px; color: white; text-shadow: 2px 2px 2px black;';
  438. const maxValue = possibleWords.length > 100 ? 100 : possibleWords.length;
  439. let hueValue = possibleWords.length > 1 ? Math.floor(360 * index / (maxValue - 1)) : 0;
  440. wordElem.style.backgroundColor = `hsl(${hueValue}, 100%, 50%)`;
  441.  
  442. this.addHoverEffect(wordElem, hueValue);
  443. this.addClickFunctionality(wordElem, word, inputElem, hueValue);
  444. this.guessElem.appendChild(wordElem);
  445. });
  446. }
  447.  
  448. addHoverEffect(wordElem, hueValue) {
  449. wordElem.addEventListener('mouseenter', function() {
  450. if (!this.classList.contains('pressed')) {
  451. this.style.backgroundColor = 'lightgray';
  452. }
  453. this.classList.add('hovered');
  454. });
  455.  
  456. wordElem.addEventListener('mouseleave', function() {
  457. if (!this.classList.contains('pressed')) {
  458. this.style.backgroundColor = `hsl(${hueValue}, 100%, 50%)`;
  459. }
  460. this.classList.remove('hovered');
  461. });
  462. }
  463.  
  464.  
  465. addClickFunctionality(wordElem, word, inputElem, colorValue) {
  466. wordElem.addEventListener('mousedown', function() {
  467. wordElem.classList.add('pressed');
  468. wordElem.style.backgroundColor = 'gray';
  469. });
  470.  
  471. wordElem.addEventListener('mouseup', function() {
  472. wordElem.classList.remove('pressed');
  473. if (!wordElem.classList.contains('hovered')) {
  474. wordElem.style.backgroundColor = `rgb(${colorValue}, ${255 - colorValue}, 0)`;
  475. } else {
  476. wordElem.style.backgroundColor = 'lightgray';
  477. }
  478. });
  479.  
  480. wordElem.addEventListener('click', function() {
  481. const formElem = document.querySelector('#game-chat form');
  482. inputElem.value = word;
  483. formElem.dispatchEvent(new Event('submit', {
  484. bubbles: true,
  485. cancelable: true
  486. }));
  487. });
  488. }
  489. }
  490.  
  491. new WordSleuth();
  492. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址