Neopets - Negg Cave Advanced Solver

Dynamically solves the Negg Cave puzzle

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        Neopets - Negg Cave Advanced Solver
// @namespace   Neopets
// @match       *://www.neopets.com/shenkuu/neggcave/
// @license      MIT
// @version     1.0
// @author      God
// @description Dynamically solves the Negg Cave puzzle
// @grant       none
// ==/UserScript==

(async function() {
    'use strict';

    if (!window.location.href.includes('neopets.com/shenkuu/neggcave')) {
        return;
    }

    const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms + Math.random() * 200));

    async function waitForElement(selector, timeout = 15000) {
        const start = Date.now();
        while (Date.now() - start < timeout) {
            const element = document.querySelector(selector);
            if (element) return element;
            await delay(100);
        }
        return null;
    }

    async function simulateClick(element) {
        if (!element) return false;
        try {
            const rect = element.getBoundingClientRect();
            const clientX = rect.left + rect.width / 2;
            const clientY = rect.top + rect.height / 2;

            const mouseDown = new MouseEvent('mousedown', { bubbles: true, cancelable: true, clientX, clientY });
            const mouseUp = new MouseEvent('mouseup', { bubbles: true, cancelable: true, clientX, clientY });
            const click = new MouseEvent('click', { bubbles: true, cancelable: true, clientX, clientY });

            element.dispatchEvent(mouseDown);
            await delay(50);
            element.dispatchEvent(mouseUp);
            await delay(50);
            element.dispatchEvent(click);
            await delay(300 + Math.random() * 200);
            return true;
        } catch {
            return false;
        }
    }

    async function clickElement(selector, retries = 3) {
        for (let attempt = 1; attempt <= retries; attempt++) {
            const element = await waitForElement(selector);
            if (!element) {
                if (attempt === retries) return false;
                await delay(500);
                continue;
            }
            if (await simulateClick(element)) {
                return true;
            }
            if (attempt === retries) return false;
            await delay(500);
        }
        return false;
    }

    async function fillCell(row, col, symbol, color) {
        if (!await clickElement(`#mnc_parch_ui_symbol_${symbol}`)) return false;
        if (!await clickElement(`#mnc_parch_ui_color_${color}`)) return false;
        if (!await clickElement(`#mnc_grid_cell_${row}_${col}`)) return false;
        if (!await clickElement('#mnc_parch_ui_clear')) return false;

        const cell = await waitForElement(`#mnc_grid_cell_${row}_${col}`);
        if (!cell) return false;
        const expectedClass = `mnc_negg_cell_s${symbol}c${color}`;
        if (!cell.classList.contains(expectedClass)) return false;
        return true;
    }

    async function resetGrid() {
        if (!await clickElement('#mnc_parch_ui_reset')) return false;
        window.confirm = () => true;
        await delay(1000);
        return true;
    }

    async function submitGrid() {
        const submitButton = await waitForElement('#mnc_negg_submit_text');
        if (!submitButton || submitButton.classList.contains('disabled')) return false;
        return await simulateClick(submitButton);
    }

    function parseClues() {
        const clueTables = document.querySelectorAll('#mnc_parch_clues .mnc_clue_table');
        const clues = [];
        clueTables.forEach(table => {
            const rows = table.querySelectorAll('tbody tr');
            const clue = [];
            rows.forEach(row => {
                const cells = row.querySelectorAll('td');
                const clueRow = [];
                cells.forEach(cell => {
                    const div = cell.querySelector('div');
                    if (!div) {
                        clueRow.push({ symbol: -1, color: -1 });
                        return;
                    }
                    const className = div.className;
                    const symbolMatch = className.match(/s([0-2X])c/);
                    const colorMatch = className.match(/c([0-2X])/);
                    const symbol = symbolMatch[1] === 'X' ? -1 : parseInt(symbolMatch[1]);
                    const color = colorMatch[1] === 'X' ? -1 : parseInt(colorMatch[1]);
                    clueRow.push({ symbol, color });
                });
                clue.push(clueRow);
            });
            clues.push(clue);
        });
        return clues;
    }

    function canPlaceClue(grid, clue, rowOffset, colOffset) {
        for (let r = 0; r < clue.length; r++) {
            for (let c = 0; c < clue[r].length; c++) {
                const gridRow = rowOffset + r;
                const gridCol = colOffset + c;
                if (gridRow >= 3 || gridCol >= 3) return false;
                const clueCell = clue[r][c];
                const gridCell = grid[gridRow][gridCol];
                if (clueCell.symbol === -1 && clueCell.color === -1) continue;
                if (clueCell.symbol !== -1 && gridCell.symbol !== -1 && gridCell.symbol !== clueCell.symbol) return false;
                if (clueCell.color !== -1 && gridCell.color !== -1 && gridCell.color !== clueCell.color) return false;
            }
        }
        return true;
    }

    function placeClue(grid, clue, rowOffset, colOffset) {
        const newGrid = JSON.parse(JSON.stringify(grid));
        for (let r = 0; r < clue.length; r++) {
            for (let c = 0; c < clue[r].length; c++) {
                const gridRow = rowOffset + r;
                const gridCol = colOffset + c;
                if (gridRow >= 3 || gridCol >= 3) continue;
                const clueCell = clue[r][c];
                if (clueCell.symbol !== -1) newGrid[gridRow][gridCol].symbol = clueCell.symbol;
                if (clueCell.color !== -1) newGrid[gridRow][gridCol].color = clueCell.color;
            }
        }
        return newGrid;
    }

    function countOccurrences(grid) {
        const symbolCount = { 0: 0, 1: 0, 2: 0 };
        const colorCount = { 0: 0, 1: 0, 2: 0 };
        const symbolColorCount = {
            0: { 0: 0, 1: 0, 2: 0 },
            1: { 0: 0, 1: 0, 2: 0 },
            2: { 0: 0, 1: 0, 2: 0 }
        };
        for (let r = 0; r < 3; r++) {
            for (let c = 0; c < 3; c++) {
                const cell = grid[r][c];
                if (cell.symbol !== -1) symbolCount[cell.symbol]++;
                if (cell.color !== -1) colorCount[cell.color]++;
                if (cell.symbol !== -1 && cell.color !== -1) symbolColorCount[cell.symbol][cell.color]++;
            }
        }
        return { symbolCount, colorCount, symbolColorCount };
    }

    function solvePuzzle(clues) {
        let grid = Array(3).fill().map(() => Array(3).fill().map(() => ({ symbol: -1, color: -1 })));
        clues.sort((a, b) => (b.length * b[0].length) - (a.length * a[0].length));

        function placeCluesRecursively(clueIndex) {
            if (clueIndex === clues.length) {
                return fillRemainingCells(grid);
            }

            const clue = clues[clueIndex];
            const clueRows = clue.length;
            const clueCols = clue[0].length;

            for (let row = 0; row <= 3 - clueRows; row++) {
                for (let col = 0; col <= 3 - clueCols; col++) {
                    if (canPlaceClue(grid, clue, row, col)) {
                        const newGrid = placeClue(grid, clue, row, col);
                        const savedGrid = JSON.parse(JSON.stringify(grid));
                        grid = newGrid;
                        const result = placeCluesRecursively(clueIndex + 1);
                        if (result) return result;
                        grid = savedGrid;
                    }
                }
            }
            return null;
        }

        function fillRemainingCells(tempGrid) {
            let grid = JSON.parse(JSON.stringify(tempGrid));
            let attempts = 0;
            const maxAttempts = 100;

            while (attempts < maxAttempts) {
                attempts++;
                let changes = false;
                const { symbolCount, colorCount, symbolColorCount } = countOccurrences(grid);

                let isFullyFilled = true;
                for (let r = 0; r < 3; r++) {
                    for (let c = 0; c < 3; c++) {
                        if (grid[r][c].symbol === -1 || grid[r][c].color === -1) {
                            isFullyFilled = false;
                            break;
                        }
                    }
                    if (!isFullyFilled) break;
                }

                if (isFullyFilled) {
                    if (Object.values(symbolCount).every(count => count === 3) &&
                        Object.values(colorCount).every(count => count === 3)) {
                        return grid;
                    }
                    return null;
                }

                for (let r = 0; r < 3; r++) {
                    for (let c = 0; c < 3; c++) {
                        let cell = grid[r][c];
                        if (cell.symbol !== -1 && cell.color === -1) {
                            const symbol = cell.symbol;
                            const usedColors = Object.keys(symbolColorCount[symbol])
                                .filter(color => symbolColorCount[symbol][color] > 0)
                                .map(Number);
                            const availableColors = [0, 1, 2].filter(color => !usedColors.includes(color) && colorCount[color] < 3);
                            if (availableColors.length === 1) {
                                cell.color = availableColors[0];
                                changes = true;
                            } else if (availableColors.length === 0) {
                                return null;
                            }
                        } else if (cell.color !== -1 && cell.symbol === -1) {
                            const color = cell.color;
                            const usedSymbols = Object.keys(symbolColorCount)
                                .filter(symbol => symbolColorCount[symbol][color] > 0)
                                .map(Number);
                            const availableSymbols = [0, 1, 2].filter(symbol => !usedSymbols.includes(symbol) && symbolCount[symbol] < 3);
                            if (availableSymbols.length === 1) {
                                cell.symbol = availableSymbols[0];
                                changes = true;
                            } else if (availableSymbols.length === 0) {
                                return null;
                            }
                        } else if (cell.symbol === -1 && cell.color === -1) {
                            const missingSymbols = Object.keys(symbolCount).filter(s => symbolCount[s] < 3).map(Number);
                            const missingColors = Object.keys(colorCount).filter(c => colorCount[c] < 3).map(Number);
                            const possiblePairs = [];
                            for (const s of missingSymbols) {
                                for (const c of missingColors) {
                                    if (symbolColorCount[s][c] === 0) {
                                        possiblePairs.push({ symbol: s, color: c });
                                    }
                                }
                            }
                            if (possiblePairs.length === 1) {
                                cell.symbol = possiblePairs[0].symbol;
                                cell.color = possiblePairs[0].color;
                                changes = true;
                            }
                        }
                        grid[r][c] = cell;
                    }
                }

                if (!changes) {
                    for (let r = 0; r < 3; r++) {
                        for (let c = 0; c < 3; c++) {
                            if (grid[r][c].symbol === -1 && grid[r][c].color === -1) {
                                const { symbolCount: sc, colorCount: cc, symbolColorCount: scc } = countOccurrences(grid);
                                const missingSymbols = Object.keys(sc).filter(s => sc[s] < 3).map(Number);
                                const missingColors = Object.keys(cc).filter(c => cc[c] < 3).map(Number);
                                for (const s of missingSymbols) {
                                    for (const c of missingColors) {
                                        if (scc[s][c] === 0) {
                                            const newGrid = JSON.parse(JSON.stringify(grid));
                                            newGrid[r][c] = { symbol: s, color: c };
                                            const result = fillRemainingCells(newGrid);
                                            if (result) return result;
                                        }
                                    }
                                }
                                return null;
                            }
                        }
                    }
                }
            }
            return null;
        }

        return placeCluesRecursively(0);
    }

    let maxAttempts = 3;
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
        const grid = await waitForElement('#mnc_negg_grid');
        if (!grid) return;

        if (!await resetGrid()) continue;

        const clues = parseClues();
        if (!clues.length) continue;

        const solution = solvePuzzle(clues);
        if (!solution) continue;

        let fillSuccess = true;
        for (let row = 0; row < 3; row++) {
            for (let col = 0; col < 3; col++) {
                const cell = solution[row][col];
                if (!await fillCell(row, col, cell.symbol, cell.color)) {
                    fillSuccess = false;
                    break;
                }
            }
            if (!fillSuccess) break;
        }

        if (!fillSuccess) continue;

        await delay(3000 + Math.random() * 1000);
        if (await submitGrid()) {
            return;
        }
    }
})();