Wordle Tools

Some enhancements of the Wordle game.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Wordle Tools
// @namespace    CCCC_David
// @version      0.2.0
// @description  Some enhancements of the Wordle game.
// @author       CCCC_David
// @match        https://www.powerlanguage.co.uk/wordle/
// @match        https://www.nytimes.com/games/wordle/index.html
// @grant        none
// @license      MIT
// ==/UserScript==

(async () => {
    'use strict';

    const SUCCESS_MSG_TIMEOUT = 3000;

    const allowedPolicy = window.trustedTypes?.createPolicy?.('allowedPolicy', {createHTML: (x) => x});
    const createTrustedHTML = (html) => (allowedPolicy ? allowedPolicy.createHTML(html) : html);

    let puzzleList = null;

    const getPuzzleList = async () => {
        if (puzzleList) {
            return puzzleList;
        }
        try {
            for (const el of document.getElementsByTagName('script')) {
                const scriptSrc = el.src;
                if (/\bmain\.\w+\.js$/u.test(scriptSrc ?? '')) {
                    // eslint-disable-next-line no-await-in-loop
                    const res = await fetch(scriptSrc, {
                        method: 'GET',
                        mode: 'same-origin',
                        redirect: 'follow',
                    });
                    // eslint-disable-next-line no-await-in-loop
                    const jsCode = await res.text();
                    puzzleList = JSON.parse(jsCode.match(/\["cigar".*?"shave"\]/u)[0]);
                    return puzzleList;
                }
            }
        } catch (e) {
            // eslint-disable-next-line no-console
            console.error(e);
        }
        return null;
    };

    const appendPuzzleIdToTitle = (gameAppElement, puzzleId) => {
        const puzzleIdInTitleSpan = document.createElement('span');
        puzzleIdInTitleSpan.id = 'wordle-tools-puzzle-id-in-title';
        puzzleIdInTitleSpan.innerText = puzzleId;
        gameAppElement.$game.parentElement.querySelector('div[class="title"]').appendChild(puzzleIdInTitleSpan);
    };

    const clearGameState = (gameAppElement) => {
        localStorage.removeItem('gameState');
        localStorage.removeItem('nyt-wordle-state');
        gameAppElement.gameStatus = 'IN_PROGRESS';
        gameAppElement.canInput = true;
        gameAppElement.boardState = new Array(6).fill('');
        gameAppElement.evaluations = new Array(6).fill(null);
        gameAppElement.letterEvaluations = {};
        gameAppElement.rowIndex = 0;
        gameAppElement.tileIndex = 0;
        gameAppElement.restoringFromLocalStorage = false;
        for (const row of gameAppElement.$game.getElementsByTagName('game-row')) {
            row.removeAttribute('letters');
            for (const tile of row.shadowRoot.querySelectorAll('game-tile')) {
                tile.removeAttribute('letter');
                tile.removeAttribute('evaluation');
                tile.removeAttribute('reveal');
            }
        }
        for (const button of gameAppElement.$keyboard.shadowRoot.querySelectorAll('button')) {
            button.removeAttribute('data-state');
            button.classList.remove('fade');
        }
        gameAppElement.$game.querySelector('#game-toaster').innerHTML = '';
    };

    const clearStatistics = () => {
        localStorage.removeItem('statistics');
        localStorage.removeItem('nyt-wordle-statistics');
    };

    const jumpToPuzzleId = (gameAppElement, settingsShadowRoot, puzzleId) => {
        clearGameState(gameAppElement);
        gameAppElement.dayOffset = puzzleId;
        gameAppElement.solution = puzzleList[puzzleId % puzzleList.length];
        gameAppElement.$game.parentElement.querySelector('#wordle-tools-puzzle-id-in-title').innerText = puzzleId;
        settingsShadowRoot.getElementById('puzzle-number').innerText = `#${puzzleId}`;
    };

    const handleGameAppElement = async (gameAppElement) => {
        if (!gameAppElement) {
            return;
        }

        window.gameApp = gameAppElement;
        await getPuzzleList();

        const appendSettingsItems = (gameSettingsElement) => {
            if (!gameSettingsElement) {
                return;
            }

            const settingsShadowRoot = gameSettingsElement.shadowRoot;
            const settingsSection = settingsShadowRoot.querySelector('div[class="sections"] > section');

            const showSuccessMsg = (message, insertBeforeNode, enableElements) => {
                const successMsgElement = document.createElement('span');
                successMsgElement.innerText = message;
                successMsgElement.style.color = 'var(--color-correct)';
                insertBeforeNode.parentElement.insertBefore(successMsgElement, insertBeforeNode);
                setTimeout(() => {
                    successMsgElement.remove();
                    for (const el of enableElements) {
                        el.disabled = false;
                    }
                }, SUCCESS_MSG_TIMEOUT);
            };

            const jumpItem = document.createElement('div');
            settingsSection.appendChild(jumpItem);
            jumpItem.outerHTML = createTrustedHTML(`
                <div class="setting">
                    <div class="text">
                        <div class="title">Jump to Puzzle #</div>
                    </div>
                    <div class="control">
                        <input type="text" id="wordle-tools-jump-target-id" style="width: 3em;">
                        <button id="wordle-tools-jump-to-puzzle">Jump</button>
                    </div>
                </div>
            `);
            const jumpButton = settingsShadowRoot.getElementById('wordle-tools-jump-to-puzzle');
            const jumpInput = settingsShadowRoot.getElementById('wordle-tools-jump-target-id');
            jumpInput.value = gameAppElement.dayOffset;
            jumpButton.addEventListener('click', (e) => {
                const button = e.target;
                const inputBox = button.parentElement.querySelector('input');
                const puzzleId = parseInt(inputBox.value, 10);
                if (Number.isNaN(puzzleId) || puzzleId < 0) {
                    inputBox.value = gameAppElement.dayOffset;
                    return;
                }
                button.disabled = true;
                inputBox.disabled = true;
                inputBox.value = puzzleId;
                jumpToPuzzleId(gameAppElement, settingsShadowRoot, puzzleId);
                showSuccessMsg('Jumped to ', inputBox, [button, inputBox]);
            });
            jumpInput.addEventListener('keydown', (e) => {
                if (e.key === 'Enter') {
                    jumpButton.click();
                }
            });

            const clearStateItem = document.createElement('div');
            settingsSection.appendChild(clearStateItem);
            clearStateItem.outerHTML = createTrustedHTML(`
                <div class="setting">
                    <div class="text">
                        <div class="title">Clear Current Game State</div>
                    </div>
                    <div class="control">
                        <button id="wordle-tools-clear-game-state">Clear</button>
                    </div>
                </div>
            `);
            settingsShadowRoot.getElementById('wordle-tools-clear-game-state').addEventListener('click', (e) => {
                const button = e.target;
                button.disabled = true;
                clearGameState(gameAppElement);
                showSuccessMsg('Cleared ', button, [button]);
            });

            const clearStatsItem = document.createElement('div');
            settingsSection.appendChild(clearStatsItem);
            clearStatsItem.outerHTML = createTrustedHTML(`
                <div class="setting">
                    <div class="text">
                        <div class="title">Clear Game Statistics</div>
                    </div>
                    <div class="control">
                        <button id="wordle-tools-clear-statistics">Clear</button>
                    </div>
                </div>
            `);
            settingsShadowRoot.getElementById('wordle-tools-clear-statistics').addEventListener('click', (e) => {
                const button = e.target;
                button.disabled = true;
                clearStatistics();
                showSuccessMsg('Cleared ', button, [button]);
            });

            const feedbackSection = settingsShadowRoot.querySelectorAll('div[class="sections"] > section')[1];
            const wordleToolsMarker = document.createElement('div');
            feedbackSection.appendChild(wordleToolsMarker);
            wordleToolsMarker.outerHTML = createTrustedHTML(`
                <div class="setting">
                    <div class="text">
                        <div class="title">Wordle Tools v0.2.0 Enabled</div>
                    </div>
                </div>
            `);
        };

        const gameSettingsObserver = new MutationObserver((mutationsList) => {
            for (const mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    for (const el of mutation.addedNodes) {
                        if (el.nodeName.toLowerCase() === 'game-settings') {
                            appendSettingsItems(el);
                        }
                    }
                }
            }
        });

        gameSettingsObserver.observe(gameAppElement.$game, {
            subtree: true,
            childList: true,
        });

        appendSettingsItems(gameAppElement.$game.getElementsByTagName('game-settings')[0]);
        appendPuzzleIdToTitle(gameAppElement, gameAppElement.dayOffset);
    };

    const gameAppObserver = new MutationObserver(async (mutationsList) => {
        for (const mutation of mutationsList) {
            if (mutation.type === 'childList') {
                for (const el of mutation.addedNodes) {
                    if (el.nodeName.toLowerCase() === 'game-app') {
                        // eslint-disable-next-line no-await-in-loop
                        await handleGameAppElement(el);
                    }
                }
            }
        }
    });

    gameAppObserver.observe(document.documentElement, {
        subtree: true,
        childList: true,
    });

    await handleGameAppElement(document.getElementsByTagName('game-app')[0]);
})();