Skip to content

Instantly share code, notes, and snippets.

@q00u
Last active May 1, 2024 01:08
Show Gist options
  • Save q00u/f7f61ac36ea08fea8b27f87cbd3050f7 to your computer and use it in GitHub Desktop.
Save q00u/f7f61ac36ea08fea8b27f87cbd3050f7 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Share Wordle Guesses
// @namespace https://gist.github.com/q00u
// @version 0.9
// @downloadURL https://gist.github.com/q00u/f7f61ac36ea08fea8b27f87cbd3050f7/raw/ShareWordleGuesses.user.js
// @updateURL https://gist.github.com/q00u/f7f61ac36ea08fea8b27f87cbd3050f7/raw/ShareWordleGuesses.user.js
// @supportURL https://gist.github.com/q00u/f7f61ac36ea08fea8b27f87cbd3050f7
// @description Copy Wordle guesses (but not _answer_) for Slack comment
// @author Phoenix G
// @match https://www.nytimes.com/games/wordle/index.html
// @icon https://www.google.com/s2/favicons?sz=64&domain=nytimes.com
// @grant unsafeWindow
// ==/UserScript==
(async function() {
'use strict';
// Your code here...
const grabGuesses = (wordleState) => {
console.log('wordleState:', wordleState);
// Build output strings
const output = [];
let finalOut = '';
let done = false;
// Dangit, evaluation is no longer stored in localStorage
const board = document.querySelectorAll("[class*=Board-module_board_]")[0];
for (let i=0; i<6 && !done; i++) {
let thisLine = ''; // The potential output
let altLine = ''; // Alternative line for *nearly* correct guesses
let correctCount = 0;
//let word = wordleState.game.boardState[i]; // The line as-is
let word = wordleState[i]; // The line as-is
const row = board.childNodes[i];
let allCorrect = true;
// For each letter...
for (let j=0; j<5; j++) {
const c = word[j];
const evaluation = row.childNodes[j].firstChild.dataset.state;
switch(evaluation) {
case 'absent':
case 'present':
allCorrect = false;
thisLine += `${c}`;
altLine += `${c}`;
break;
case 'correct':
thisLine += `${c.toUpperCase()}`;
altLine += '-';
correctCount += 1;
break;
case 'tbd': break; //Correct line, while it is animating. If we see this, we're done
default: throw 'Unexpected row evaluation: ' + evaluation;
}
}
// const evaluation = wordleState.evaluations[i]; // The evaluation for this word
// // console.log('Evaluating line:', word, evaluation);
// let allCorrect = true;
// // For each letter...
// for (let j=0; j<5; j++) {
// const c = word[j];
// switch(evaluation[j]) {
// case 'absent':
// case 'present': // Slack can't apply italics inside code lines nor around letters that touch
// allCorrect = false;
// thisLine += `${c}`;
// break;
// case 'correct': thisLine += `${c.toUpperCase()}`; break;
// default: throw 'Unexpected row evaluation : ' + evaluation[j];
// }
// }
// Wait, was this the answer?
if (allCorrect) {
// We're done, pretty-print results
//let finalOut = '';
for (let k=0; k<output.length; k++) {
finalOut += `\`${output[k]}\`\n`;
}
console.log(finalOut);
done = true;
} else {
// Otherwise, add to output and continue
if (correctCount < 4) {
output.push(thisLine);
} else {
output.push(altLine);
}
}
}
// Add snackbar for copy button to use later
const snackbarDiv = document.createElement('div');
snackbarDiv.id = 'snackbar';
snackbarDiv.innerText = 'Guesses copied!';
document.body.append(snackbarDiv);
// Style it
function GM_addStyle(css) {
const style = document.getElementById("GM_addStyleBy8626") || (function() {
const style = document.createElement('style');
style.type = 'text/css';
style.id = "GM_addStyleBy8626";
document.head.appendChild(style);
return style;
})();
const sheet = style.sheet;
sheet.insertRule(css, (sheet.rules || sheet.cssRules || []).length);
}
// Position snackbar at the bottom and in the middle of the screen
GM_addStyle('#snackbar{background-color:var(--modal-content-bg);border:1px solid var(--color-tone-6);border-radius:2px;box-shadow:0 4px 23px 0 rgba(0,0,0,.2);color:var(--color-tone-1);height:18px;margin-left:-60px;min-width:120px;padding:16px;position:fixed;right:24px;text-align:center;top:64px;visibility:hidden;z-index:var(--modal-z-index)}');
// Show the snackbar when clicking a button
GM_addStyle('#snackbar.show{animation:.5s fadein,.5s 2.5s fadeout;visibility:visible;webkit-animation:fadein 0.5s,fadeout 0.5s 2.5s}');
// Animations to fade the snackbar in and out
GM_addStyle('@-webkit-keyframes fadein {from {bottom: 0;opacity: 0;}to {bottom: 30px;opacity: 1;}}');
GM_addStyle('@keyframes fadein {from {bottom: 0;opacity: 0;}to {bottom: 30px;opacity: 1;}}');
GM_addStyle('@-webkit-keyframes fadeout {from {bottom: 30px;opacity: 1;}to {bottom: 0;opacity: 0;}}');
GM_addStyle('@keyframes fadeout {from {bottom: 30px;opacity: 1;}to {bottom: 0;opacity: 0;}}');
// Build SVG
const xmlns = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(xmlns, 'svg');
svg.setAttribute('width', '24px');
svg.setAttribute('height', '24px');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('class', 'game-icon');
svg.setAttribute('data-testid', 'icon-copy');
svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', 'http://www.w3.org/1999/xlink');
const rect1 = document.createElementNS(xmlns, 'rect');
rect1.setAttribute('x', '2');
rect1.setAttribute('y', '1');
rect1.setAttribute('rx', '1');
rect1.setAttribute('ry', '1');
rect1.setAttribute('width', '16');
rect1.setAttribute('height', '20');
rect1.setAttribute('style', 'fill: none; stroke:var(--color-tone-1); strike-width: 1');
const rect2 = document.createElementNS(xmlns, 'rect');
rect2.setAttribute('x', '5');
rect2.setAttribute('y', '5');
rect2.setAttribute('rx', '0.5');
rect2.setAttribute('ry', '0.5');
rect2.setAttribute('width', '10');
rect2.setAttribute('height', '1');
rect2.setAttribute('style', 'fill:var(--color-tone-1)');
const rect3 = document.createElementNS(xmlns, 'rect');
rect3.setAttribute('x', '5');
rect3.setAttribute('y', '8');
rect3.setAttribute('rx', '0.5');
rect3.setAttribute('ry', '0.5');
rect3.setAttribute('width', '10');
rect3.setAttribute('height', '1');
rect3.setAttribute('style', 'fill:var(--color-tone-1)');
const rect4 = document.createElementNS(xmlns, 'rect');
rect4.setAttribute('x', '5');
rect4.setAttribute('y', '11');
rect4.setAttribute('rx', '0.5');
rect4.setAttribute('ry', '0.5');
rect4.setAttribute('width', '10');
rect4.setAttribute('height', '1');
rect4.setAttribute('style', 'fill:var(--color-tone-1)');
const rect5 = document.createElementNS(xmlns, 'rect');
rect5.setAttribute('x', '5');
rect5.setAttribute('y', '14');
rect5.setAttribute('rx', '0.5');
rect5.setAttribute('ry', '0.5');
rect5.setAttribute('width', '6');
rect5.setAttribute('height', '1');
rect5.setAttribute('style', 'fill:var(--color-tone-1)');
const path1 = document.createElementNS(xmlns, 'path');
path1.setAttribute('d', 'M18,3h3a1,1 0 0 1 1,1v18a-1,1 0 0 1 -1,1h-14a-1,-1 0 0 1 -1,-1v-1');
path1.setAttribute('style', 'fill: none; stroke: var(--color-tone-1); stroke-width: 1');
svg.appendChild(rect1);
svg.appendChild(rect2);
svg.appendChild(rect3);
svg.appendChild(rect4);
svg.appendChild(rect5);
svg.appendChild(path1);
//console.log(svg);
let btn = document.createElement('button');
btn.appendChild(svg);
//btn.onclick = copyToClipBoard(finalOut);
btn.addEventListener('click', function() {
const el = document.createElement('textarea');
el.value = finalOut;
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
//console.log('Copied to clipboard:', el.value);
// Now show the snackbar
snackbarDiv.className = 'show';
setTimeout(function(){
snackbarDiv.className = snackbarDiv.className.replace('show','');
}, 3000);
});
btn.type = 'button';
btn.id = 'copy-button';
//btn.class = 'AppHeader-module_icon__x7b46';
btn.ariaLabel = 'Copy Guesses';
btn.tabIndex = -1;
// Find the header
let appHeader = document.getElementsByClassName('wordle-app-header')[0];
let menuRight;
if (appHeader === undefined) {
// Header tag is missing its class, probably in old style
console.log('Header tag has no class, trying alternate route...');
appHeader = document.querySelector('game-app').shadowRoot.querySelector('game-theme-manager').querySelector('header');
menuRight = appHeader.children[2];
btn.className = 'icon';
} else {
// Last child should be menuRight
menuRight = appHeader.lastChild;
// Grab class from first child of menuRight
const btnClass = menuRight.firstChild.className;
// Set class of button
btn.className = btnClass;
}
// Add new button to start of menuRight
menuRight.prepend(btn);
}
const waitForGame = (rightKey) => {
// WAIT FOR PUZZLE TO BE SOLVED
console.log('Waiting for game to resolve.');
// Grab board state from local storage
//const wordleState = JSON.parse(unsafeWindow.localStorage['nyt-wordle-state']);
// Updated 2022/10/18:
//const wordleState = JSON.parse(unsafeWindow.localStorage['nyt-wordle-moogle/170771851']);
//console.log('wordleState:', typeof wordleState, wordleState);
const repeatcheck = () => {
//const wordleState = JSON.parse(unsafeWindow.localStorage['nyt-wordle-state']);
//console.log('rightKey in waitForGame:', rightKey);
const wordleState = JSON.parse(unsafeWindow.localStorage[rightKey]);
//const wordleState = {game: {status: 'FAIL'}};
//if (wordleState.gameStatus != 'IN_PROGRESS') {
//if (wordleState.game.status === 'FAIL' || wordleState.game.status === 'WIN') {
if (wordleState.states[0].data.status === 'FAIL' || wordleState.states[0].data.status === 'WIN') {
console.log('Game has ended!');
clearInterval(clearcheck);
//grabGuesses(wordleState);
grabGuesses(wordleState.states[0].data.boardState);
}
}
const clearcheck = setInterval(repeatcheck, 500);
}
const waitForWelcome = (rightKey) => {
// WAIT FOR WELCOME SCREEN TO CLOSE
console.log('Waiting for Welcome screen to close...');
const repeatcheck = () => {
const welcome = document.querySelectorAll("[class*=Welcome-module_contentWelcome]");
if (welcome.length === 0) {
console.log('Welcome!');
clearInterval(clearcheck);
waitForGame(rightKey);
}
};
const clearcheck = setInterval(repeatcheck, 500);
}
const waitForLocalStorage = () => {
// Find right localstorage
console.log('Waiting for localStorage to resolve...');
const repeatcheck = () => {
const lstorage = unsafeWindow.localStorage;
const keys = Object.keys(lstorage);
//console.log(keys.length);
if (keys.length > 0) {
console.log('localStorage ready!');
// Look for most recent timestamp
let latest = 0;
let rightKey;
keys.forEach((key) => {
// if (key.startsWith('nyt-wordle-moogle')) {
if (key.startsWith('games-state-wordleV2')) {
const here = JSON.parse(lstorage[key]);
// if (here.timestamp > latest) {
if (here.states[0].timestamp > latest) {
latest = here.states[0].timestamp;
rightKey = key;
}
}
});
console.log('Checking localStorage:', rightKey);
clearInterval(clearcheck);
waitForWelcome(rightKey);
}
};
const clearcheck = setInterval(repeatcheck, 500);
}
//Begin
waitForLocalStorage();
})();
@q00u
Copy link
Author

q00u commented Jul 2, 2022

0.1: Initial version

  • Checks game data on page, if game is complete it pretty-prints guesses to consol

0.2: Big Change

  • Instead of grabbing guesses from the display area (which changed with the wordle layout update), grab them from the browser's local storage (where game data is saved, and it shouldn't change)

0.3: Big Change

  • Wait until game has finished before processing! No more reloading!
  • Add copy button to header, click and copy directly! No more console!

0.4:

  • Add snackbar message about guesses being copied when clicking on the copy button
  • Wordle has two (slightly different) versions at the moment, and which you get seems random. Adjust accordingly.

0.5:

  • Clean up commented code and console.log

0.6:

  • Fix bug where if all guess letters are present or correct, it incorrectly thinks they're all correct and stops. It looks like this bug was introduced in 0.2.

0.7:

  • Update to account for changes on Wordle website
  • Waits to get past welcome screen if logged in
  • Wait for localStorage
  • Look for the right localStorage (whatever is latest)
  • Specifically wait for "WIN" or "FAIL" state, in case game.status didn't load
  • Letter status is no longer in localStorage, go back to checking DOM

0.8:

  • Update to mask very-nearly-correct answers (ie, 4/5 correct letter placements) to be less spoilery
  • Added UserScript update URL

0.9:

  • NYT moved Wordle's state location in localstorage

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment