Last active
March 11, 2026 18:02
-
-
Save Nouchey/19395c9f24be93170048ef43aa8daafa to your computer and use it in GitHub Desktop.
Wordle Hardcore Mode (Tampermonkey Userscript)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Wordle Hardcore Mode | |
| // @description Adds a more restrictive “Hard Mode” to New York Times Games' Wordle to warn the player about impossible guesses | |
| // @version 2026-03-04 | |
| // @author Nouche | |
| // @match https://www.nytimes.com/games/*wordle* | |
| // @run-at document-start | |
| // @tag games | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| function parseGrid() { | |
| const grid = { | |
| word: '', | |
| history: [], | |
| }; | |
| const rowEls = document.querySelectorAll('[class^="Board-"] > [class^="Row-"]'); | |
| for (const rowEl of rowEls) { | |
| const slotEls = rowEl.querySelectorAll('[class^="Tile-"]'); | |
| if (['tbd', 'empty'].includes(Array.from(slotEls).at(-1).getAttribute('data-state'))) { | |
| // Current guess | |
| for (const slotEl of slotEls) { | |
| grid.word += slotEl.textContent.trim().toUpperCase(); | |
| } | |
| break; // no point processing further empty rows | |
| } else { | |
| // Prior guess | |
| const entry = []; | |
| for (const slotEl of slotEls) { | |
| let color; | |
| switch (slotEl.getAttribute('data-state')) { | |
| case 'correct': | |
| color = 'green'; | |
| break; | |
| case 'present': | |
| color = 'yellow'; | |
| break; | |
| case 'absent': | |
| default: | |
| color = 'gray'; | |
| break; | |
| } | |
| entry.push({ | |
| letter: slotEl.textContent.trim().toUpperCase(), | |
| color, | |
| }); | |
| } | |
| grid.history.push(entry); | |
| } | |
| } | |
| return grid; | |
| } | |
| function checkRequirements(grid) { | |
| const counts = { }; | |
| for (const letter of grid.word) { | |
| if (!(letter in counts)) { | |
| counts[letter] = 0; | |
| } | |
| counts[letter]++; | |
| } | |
| for (const entry of grid.history) { | |
| const entryCounts = { }; | |
| for (const slot of entry) { | |
| if (['green', 'yellow'].includes(slot.color)) { | |
| const letter = slot.letter; | |
| if (!(letter in entryCounts)) { | |
| entryCounts[letter] = 0; | |
| } | |
| entryCounts[letter]++; | |
| } | |
| } | |
| for (const letter in entryCounts) { | |
| const count = counts[letter] ?? 0; | |
| const entryCount = entryCounts[letter]; | |
| if (count < entryCount) { | |
| return { | |
| letter, | |
| count: entryCount, | |
| }; | |
| } | |
| } | |
| } | |
| } | |
| function checkEstablishments(grid) { | |
| const length = grid.history[0].length; | |
| for (let i = 0; i < length; i++) { | |
| const letter = grid.word[i]; | |
| for (const entry of grid.history) { | |
| const slot = entry[i]; | |
| if (slot.color === 'green' && slot.letter !== letter) { | |
| return { | |
| letter: slot.letter, | |
| position: i, | |
| }; | |
| } | |
| } | |
| } | |
| } | |
| function checkBans(grid) { | |
| const counts = { }; | |
| for (const letter of grid.word) { | |
| if (!(letter in counts)) { | |
| counts[letter] = 0; | |
| } | |
| counts[letter]++; | |
| } | |
| for (const letter in counts) { | |
| const count = counts[letter]; | |
| for (const entry of grid.history) { | |
| let foundCount = 0; | |
| let grayCount = 0; | |
| for (const slot of entry) { | |
| if (slot.letter === letter) { | |
| foundCount++; | |
| if (slot.color === 'gray') { | |
| grayCount++; | |
| } | |
| } | |
| } | |
| if (grayCount === 0) continue; | |
| const validCount = foundCount - grayCount; | |
| if (count > validCount) { | |
| return { | |
| letter, | |
| count: validCount, | |
| }; | |
| } | |
| } | |
| } | |
| } | |
| function checkMisplacements(grid) { | |
| const length = grid.history[0].length; | |
| for (let i = 0; i < length; i++) { | |
| const letter = grid.word[i]; | |
| for (const entry of grid.history) { | |
| const slot = entry[i]; | |
| if (['yellow', 'gray'].includes(slot.color) && slot.letter === letter) { | |
| return { | |
| letter, | |
| position: i, | |
| }; | |
| } | |
| } | |
| } | |
| } | |
| function findOversight() { | |
| const grid = parseGrid(); | |
| if (!grid.history[0] || grid.word.length < grid.history[0].length) return; | |
| const requirement = checkRequirements(grid); | |
| if (requirement) return `The “${requirement.letter}” must ${requirement.count <= 1 ? 'appear' : 'be used at least ' + (requirement.count == 2 ? 'twice' : requirement.count + ' times')}.`; | |
| const establishment = checkEstablishments(grid); | |
| if (establishment) return `The ${formatOrdinal(establishment.position + 1)} letter must be “${establishment.letter}”.`; | |
| const ban = checkBans(grid); | |
| if (ban) return `The “${ban.letter}” cannot ${ban.count <= 0 ? 'be used' : 'appear more than ' + (ban.count == 1 ? 'once' : ban.count == 2 ? 'twice' : ban.count + ' times')}.`; | |
| const misplacement = checkMisplacements(grid); | |
| if (misplacement) return `The ${formatOrdinal(misplacement.position + 1)} letter cannot be “${misplacement.letter}”.`; | |
| } | |
| function formatOrdinal(n) { | |
| const suffixes = new Map([ | |
| ['one', 'ˢᵗ'], | |
| ['two', 'ⁿᵈ'], | |
| ['few', 'ʳᵈ'], | |
| ['other', 'ᵗʰ'], | |
| ]); | |
| const pr = new Intl.PluralRules('en-US', { type: 'ordinal' }); | |
| const rule = pr.select(n); | |
| const suffix = suffixes.get(rule); | |
| return `${n}${suffix}`; | |
| } | |
| document.addEventListener( | |
| 'keydown', | |
| (e) => { | |
| if (e.key !== 'Enter') return; | |
| const oversight = findOversight(); | |
| if (oversight) { | |
| const proceed = confirm(oversight + '\nPlay word anyway?'); | |
| if (!proceed) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| e.stopImmediatePropagation(); // this is the crucial call | |
| } | |
| } | |
| }, | |
| true, | |
| ); | |
| const copyToClipboard = navigator.clipboard.writeText.bind(navigator.clipboard); | |
| navigator.clipboard.writeText = (text) => copyToClipboard(text.replace(/^(.*\/\d+)\*?$/m, '$1**').replace(/(?<=[🟩🟨⬛].*)\s*https:\/\/www\.nytimes\.com\/games\/.*\bwordle\b.*$/, '')); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment