Neopets - Negg Cave Advanced Solver

Dynamically solves the Negg Cave puzzle

// ==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;
        }
    }
})();