// ==UserScript== // @name Neopets Scarab 21 Autoplayer // @namespace GreaseMonkey // @version 1.0 // @description Automates Scarab 21. // @author @willnjohnson // @match https://www.neopets.com/games/scarab21/index.phtml // @match https://www.neopets.com/games/scarab21/scarab21.phtml* // @grant none // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/576515/Neopets%20Scarab%2021%20Autoplayer.user.js // @updateURL https://update.greasyfork.icu/scripts/576515/Neopets%20Scarab%2021%20Autoplayer.meta.js // ==/UserScript== /* This script uses a domain-specific greedy heuristic designed for the Neopets game "Scarab 21". It does NOT attempt to predict future cards (count cards) or explore all possible outcomes — instead, it makes each move based solely on the current board state with the aim of maximizing points as early as possible. Decision priority: 1. Place the drawn card in any column that will immediately total 21. 2. Special handling for Aces (1/11) and 10-value cards (10/J/Q/K): - Try to pair with complementary totals (e.g., 10 + Ace, Ace + 10). - Favor columns close to 21 without busting. - Avoid "trap" totals that limit future moves unless beneficial. 3. If no immediate 21, choose a column that: - Keeps the total ≤ 21, - Is as high as possible without busting, - Prefers non-empty columns over empty ones in mid/late game. 4. Final fallback: first available legal column. Key characteristics: - Greedy: always aims for the highest immediate gain. - Deterministic: given the same board and card, will make the same choice. - No lookahead: does not simulate future draws. - Strategy goal: build 21s early to maximize points and free up columns. */ (function () { "use strict"; // --- Configuration --- const CONFIG = { minActionDelayMs: 700, maxActionDelayMs: 1450, minNavigationDelayMs: 1000, maxNavigationDelayMs: 2000, initialLoadDelayMs: 1200, gameBaseUrl: "https://www.neopets.com/games/scarab21/", playGameUrl: "https://www.neopets.com/games/scarab21/scarab21.phtml", autoplayStorageKey: "scarab21_autoplay_enabled", highlightColor: "magenta", highlightThickness: "4px", keybinds: { KeyZ: 0, KeyX: 1, KeyC: 2, KeyV: 3, KeyB: 4, }, overlayColor: "rgba(0, 0, 0, 0.5)", overlayZIndex: 9998, }; // --- Utility Functions --- const pauseExecution = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const getElement = (selector, context = document) => { try { return context.querySelector(selector); } catch (e) { return null; } }; const getAllElements = (selector, context = document) => { try { return context.querySelectorAll(selector); } catch (e) { return []; } }; const elementExists = (selector, context = document) => !!getElement(selector, context); const getRandomDelay = () => Math.floor(Math.random() * (CONFIG.maxActionDelayMs - CONFIG.minActionDelayMs + 1)) + CONFIG.minActionDelayMs; const getRandomNavigationDelay = () => Math.floor(Math.random() * (CONFIG.maxNavigationDelayMs - CONFIG.minNavigationDelayMs + 1)) + CONFIG.minNavigationDelayMs; async function reloadPage() { await pauseExecution(getRandomNavigationDelay()); window.location.replace("https://www.neopets.com/games/scarab21/scarab21.phtml"); } async function goBack() { await pauseExecution(getRandomNavigationDelay()); window.history.back(); } // --- Local Storage --- const getAutoplaySetting = () => { const setting = localStorage.getItem(CONFIG.autoplayStorageKey); return setting === null ? true : setting === "true"; }; const setAutoplaySetting = (enabled) => { localStorage.setItem(CONFIG.autoplayStorageKey, enabled.toString()); }; // --- UI Elements --- let autoplayToggleBtn, manualPlayModal, manualPlayNextBtn, manualPlayMessage, currentHighlightedElement = null; let columnOverlays = []; function createAutoplayToggleButton() { autoplayToggleBtn = document.createElement("button"); autoplayToggleBtn.style.cssText = `position: fixed; top: 10px; right: 10px; z-index: 10000; background-color: #333; color: white; border: 1px solid #555; padding: 8px 12px; cursor: pointer; font-size: 14px; border-radius: 5px; opacity: 0.8; transition: opacity 0.3s;`; autoplayToggleBtn.onmouseover = () => (autoplayToggleBtn.style.opacity = "1"); autoplayToggleBtn.onmouseout = () => (autoplayToggleBtn.style.opacity = "0.8"); updateAutoplayButtonText(); autoplayToggleBtn.onclick = () => { const currentSetting = getAutoplaySetting(); setAutoplaySetting(!currentSetting); updateAutoplayButtonText(); if (!currentSetting) { hideManualPlayModal(); } window.location.reload(); }; document.body.appendChild(autoplayToggleBtn); } function updateAutoplayButtonText() { if (!autoplayToggleBtn) return; const enabled = getAutoplaySetting(); autoplayToggleBtn.textContent = `Autoplay: ${enabled ? "ON" : "OFF"}`; autoplayToggleBtn.style.backgroundColor = enabled ? "#28a745" : "#dc3545"; } function createManualPlayModal() { manualPlayModal = document.createElement("div"); manualPlayModal.id = "scarab21-manual-modal"; manualPlayModal.style.cssText = `position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); z-index: 9999; background-color: #333; color: white; border: 2px solid #555; padding: 15px 20px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); font-family: sans-serif; display: none; align-items: center; gap: 15px; min-width: 250px;`; manualPlayMessage = document.createElement("span"); manualPlayMessage.style.fontSize = "16px"; manualPlayModal.appendChild(manualPlayMessage); manualPlayNextBtn = document.createElement("button"); manualPlayNextBtn.textContent = "Next Move"; manualPlayNextBtn.style.cssText = `background-color: #007bff; color: white; border: none; padding: 10px 15px; border-radius: 5px; cursor: pointer; font-size: 16px; transition: background-color 0.2s;`; manualPlayNextBtn.onmouseover = () => (manualPlayNextBtn.style.backgroundColor = "#0056b3"); manualPlayNextBtn.onmouseout = () => (manualPlayNextBtn.style.backgroundColor = "#007bff"); manualPlayModal.appendChild(manualPlayNextBtn); document.body.appendChild(manualPlayModal); } const showManualPlayModal = (msg) => { if (!manualPlayModal) createManualPlayModal(); manualPlayMessage.textContent = msg; manualPlayModal.style.display = "flex"; }; const hideManualPlayModal = () => manualPlayModal && (manualPlayModal.style.display = "none"); // --- Overlay and Keyboard Input Functions --- function createColumnOverlays(gameArea) { columnOverlays.forEach((overlay) => overlay.remove()); columnOverlays = []; const columnLinkCells = getAllElements("center > table > tbody > tr:nth-child(1) > td:nth-child(2) > table > tbody > tr:first-child > td", gameArea); if (columnLinkCells.length === 0) return; columnLinkCells.forEach((cell, index) => { const arrowLink = cell.querySelector("a"); if (!arrowLink) return; const linkRect = arrowLink.getBoundingClientRect(); const bodyRect = document.body.getBoundingClientRect(); const overlay = document.createElement("div"); overlay.className = "scarab21-column-overlay"; overlay.style.cssText = `position: absolute; top: ${linkRect.top - bodyRect.top - 118}px; left: ${linkRect.left - bodyRect.left}px; width: 60px; height: 40px; background-color: #993300; display: flex; justify-content: center; align-items: center; color: white; font-size: 24px; font-weight: bold; pointer-events: none; z-index: ${CONFIG.overlayZIndex}; border: 4px solid transparent; box-sizing: border-box;`; document.body.appendChild(overlay); const keyLabel = document.createElement("span"); const keyChar = Object.keys(CONFIG.keybinds).find((key) => CONFIG.keybinds[key] === index); keyLabel.textContent = keyChar ? keyChar.replace("Key", "") : ""; overlay.appendChild(keyLabel); columnOverlays.push(overlay); }); } function highlightOverlay(colIndex) { columnOverlays.forEach((overlay, idx) => { overlay.style.borderColor = idx === colIndex - 1 ? CONFIG.highlightColor : "transparent"; }); } function clearOverlayHighlights() { columnOverlays.forEach((overlay) => { overlay.style.borderColor = "transparent"; }); } function handleKeyboardInput(event) { if (!getAutoplaySetting() && window.location.href.includes("scarab21.phtml")) { const chosenColumnIndex0Based = CONFIG.keybinds[event.code]; if (chosenColumnIndex0Based !== undefined) { event.preventDefault(); const gameArea = getElement(SELECTORS.mainGameWrapper); if (gameArea) { const arrowLink = getElement(SELECTORS.colPlayLinks(chosenColumnIndex0Based + 1), gameArea); if (arrowLink) { hideManualPlayModal(); clearOverlayHighlights(); arrowLink.click(); } } } } } // --- Game Element Selectors --- const SELECTORS = { mainGameWrapper: ".contentModule .frame > div[style='padding:7px;']", playGameButton: "input[value='Play Scarab 21!!!']", cancelGameButton: "input[value='Cancel Current Game']", collectPointsButton: "div > a > b", congratulationsMessage: "center > b:first-child", playAgainButton: "input[value='Play Again!']", drawnCardImage: "center > table > tbody > tr > td:first-child > table:nth-of-type(3) > tbody > tr > td:nth-child(2) > img", colPointTexts: "center > table > tbody > tr > td:nth-child(2) > table > tbody > tr:nth-child(3) > td", colPlayLinks: (colIndex) => `center > table > tbody > tr:nth-child(1) > td:nth-child(2) > table > tbody > tr:first-child > td:nth-child(${colIndex}) > a`, columnArrowImage: (colIndex) => `center > table > tbody > tr:nth-child(1) > td:nth-child(2) > table > tbody > tr:first-child > td:nth-child(${colIndex}) > a > img`, cardInColumn: (colIndex) => `center > table > tbody > tr > td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(${colIndex}) > img`, secondCardInColumn: (colIndex) => `center > table > tbody > tr > td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(${colIndex}) > img:nth-of-type(2)`, errorMessageDiv: "div.errorMessage b", }; // --- Game Logic --- async function checkForErrorMessage() { const errorBoldText = getElement(SELECTORS.errorMessageDiv); if (errorBoldText && errorBoldText.textContent.includes("Error: ") && errorBoldText.closest("div.errorMessage").textContent.includes("You have been directed to this page from the wrong place!")) { await goBack(); return true; } return false; } async function handleGameInit(gameArea) { await pauseExecution(CONFIG.initialLoadDelayMs); const startBtn = getElement(SELECTORS.playGameButton); const abandonBtn = getElement(SELECTORS.cancelGameButton); if (startBtn) { startBtn.click(); await pauseExecution(getRandomNavigationDelay()); return true; } else if (abandonBtn) { abandonBtn.click(); await pauseExecution(getRandomNavigationDelay()); const retryStartBtn = getElement(SELECTORS.playGameButton); if (retryStartBtn) { retryStartBtn.click(); await pauseExecution(getRandomNavigationDelay()); return true; } else { reloadPage(); return false; } } else { if (window.location.href.includes("index.phtml")) { reloadPage(); return false; } return true; } } async function handleGameCompletion(gameArea) { hideManualPlayModal(); clearOverlayHighlights(); const collectPointsBtn = getElement(SELECTORS.collectPointsButton, gameArea); if (collectPointsBtn && collectPointsBtn.textContent.includes("Collect Points")) { collectPointsBtn.closest("a").click(); await pauseExecution(getRandomNavigationDelay()); return { action: "continue" }; } const congratsMsg = getElement(SELECTORS.congratulationsMessage, gameArea); if (congratsMsg && congratsMsg.textContent.includes("Congratulations!!!")) { window.location.href = CONFIG.playGameUrl; await pauseExecution(getRandomNavigationDelay()); return { action: "restart" }; } const replayBtn = getElement(SELECTORS.playAgainButton); if (replayBtn) { await pauseExecution(getRandomNavigationDelay()); replayBtn.click(); return { action: "restart" }; } return { action: "ongoing" }; } async function getDrawnCardData(gameArea) { const cardImg = getElement(SELECTORS.drawnCardImage, gameArea); if (!cardImg) return null; const imgSrc = cardImg.getAttribute("src"); let rawVal, mathVal; try { const filename = imgSrc.substring(imgSrc.lastIndexOf("/") + 1, imgSrc.lastIndexOf("_")); rawVal = parseInt(filename); if (isNaN(rawVal)) throw new Error("Parsed value is NaN."); } catch (e) { return null; } mathVal = rawVal === 14 ? 11 : [11, 12, 13].includes(rawVal) ? 10 : rawVal; return { raw: rawVal, math: mathVal, src: imgSrc }; } function getColumnCurrentPoints(gameArea) { const pointEls = getAllElements(SELECTORS.colPointTexts, gameArea); const points = []; pointEls.forEach((el) => points.push(el.textContent.trim())); return points; } function determineBestColumn(drawnMathVal, drawnRawVal, drawnCardSrc, currentColumnStates, gameArea) { let bestCol = -1; const parsePoints = (colState) => (typeof colState === "string" && colState.includes("or") ? { A: Number(colState.split(" or ")[0]), B: Number(colState.split(" or ")[1]) } : { A: Number(colState), B: -1 }); const colContainsCard = (idx, targetRaw, targetSuit) => Array.from(getAllElements(SELECTORS.cardInColumn(idx + 1), gameArea)).some((img) => img.getAttribute("src").substring(img.getAttribute("src").lastIndexOf("/") + 1, img.getAttribute("src").lastIndexOf(".gif")).includes(`${targetRaw}_${targetSuit}`)); const colHasTwoCards = (idx) => elementExists(SELECTORS.secondCardInColumn(idx + 1), gameArea); for (let i = 0; i < currentColumnStates.length; i++) { const { A: colA, B: colB } = parsePoints(currentColumnStates[i]); const col1Based = i + 1; if (drawnMathVal + colA === 21 || (colB !== -1 && drawnMathVal + colB === 21)) { bestCol = col1Based; break; } if (drawnRawVal === 14) { if (colA === 10 || colB === 10) { if (colContainsCard(i, 11, "spades") && drawnCardSrc.includes("14_spades")) { bestCol = col1Based; break; } if (!colHasTwoCards(i)) { bestCol = col1Based; } else if (bestCol === -1) { bestCol = col1Based; } } else if (colA === 20 || colB === 20) { bestCol = col1Based; break; } } else if (drawnMathVal === 10) { if (colA === 11 || colB === 11) { if (colContainsCard(i, 14, "spades") && drawnCardSrc.includes("11_spades")) { bestCol = col1Based; break; } if (!colHasTwoCards(i)) { bestCol = col1Based; break; } else { bestCol = col1Based; break; } } else if (colA === 0) { bestCol = col1Based; } } if (bestCol === -1 && (drawnMathVal + colA === 11 || (colB !== -1 && drawnMathVal + colB === 11))) { bestCol = col1Based; } } if (bestCol !== -1) return bestCol; let fallbackCol = -1, kSum = 10000; let effDrawnVal = drawnMathVal === 11 ? 1 : drawnMathVal; for (let i = 0; i < currentColumnStates.length; i++) { const { A: colA, B: colB } = parsePoints(currentColumnStates[i]); const col1Based = i + 1; const potSums = []; if (colA + effDrawnVal <= 21) potSums.push(colA + effDrawnVal); if (colB !== -1 && colB + effDrawnVal <= 21) potSums.push(colB + effDrawnVal); if (potSums.length > 0) { const currSum = Math.min(...potSums); if (colA === 0 && drawnRawVal === 14) { fallbackCol = col1Based; break; } if (currSum < kSum && colA !== 0 && colA !== 1) { if (colA === 10 && !colHasTwoCards(i)) continue; if (colA === 11 && currentColumnStates[i].includes("or")) continue; kSum = currSum; fallbackCol = col1Based; } } } if (fallbackCol !== -1) return fallbackCol; kSum = 10000; for (let i = 0; i < currentColumnStates.length; i++) { const { A: colA, B: colB } = parsePoints(currentColumnStates[i]); const col1Based = i + 1; effDrawnVal = drawnMathVal === 11 ? 1 : drawnMathVal; const potSums = []; if (colA + effDrawnVal <= 21) potSums.push(colA + effDrawnVal); if (colB !== -1 && colB + effDrawnVal <= 21) potSums.push(colB + effDrawnVal); if (potSums.length > 0) { const currSum = Math.min(...potSums); if (currSum < kSum && colA !== 1) { if (colA === 10 && !colHasTwoCards(i)) continue; if (colA === 0 && drawnMathVal === 10) { kSum = currSum; fallbackCol = col1Based; break; } kSum = currSum; fallbackCol = col1Based; } } } if (fallbackCol !== -1) return fallbackCol; for (let i = 0; i < currentColumnStates.length; i++) { const { A: colA, B: colB } = parsePoints(currentColumnStates[i]); const col1Based = i + 1; effDrawnVal = drawnMathVal === 11 ? 1 : drawnMathVal; if (colA + effDrawnVal <= 21 || (colB !== -1 && colB + effDrawnVal <= 21)) { fallbackCol = col1Based; break; } } return fallbackCol; } async function executeCardPlacement(chosenCol, gameArea) { clearOverlayHighlights(); const targetLink = getElement(SELECTORS.colPlayLinks(chosenCol), gameArea); if (targetLink) { targetLink.click(); await pauseExecution(getRandomNavigationDelay()); } else { reloadPage(); } } // --- Main Logic --- async function initializeAutoplayer() { createAutoplayToggleButton(); createManualPlayModal(); if (await checkForErrorMessage()) return; if (window.location.href.includes("index.phtml")) { const initiated = await handleGameInit(document); if (!initiated) return; } let mainGameWrapper = getElement(SELECTORS.mainGameWrapper); if (!mainGameWrapper) { reloadPage(); return; } createColumnOverlays(mainGameWrapper); document.addEventListener("keydown", handleKeyboardInput); while (true) { clearOverlayHighlights(); const gameStatus = await handleGameCompletion(mainGameWrapper); if (gameStatus.action !== "ongoing") return; const cardData = await getDrawnCardData(mainGameWrapper); if (!cardData) { reloadPage(); return; } const colPoints = getColumnCurrentPoints(mainGameWrapper); if (colPoints.length !== 5) { reloadPage(); return; } const chosenCol = determineBestColumn(cardData.math, cardData.raw, cardData.src, colPoints, mainGameWrapper); if (chosenCol === -1) { reloadPage(); return; } highlightOverlay(chosenCol); if (getAutoplaySetting()) { hideManualPlayModal(); await pauseExecution(getRandomDelay()); await executeCardPlacement(chosenCol, mainGameWrapper); return; } else { const columnDisplay = chosenCol; showManualPlayModal(`Place ${cardData.raw} (${cardData.math}) in Col ${columnDisplay}.`); await new Promise((resolve) => { manualPlayNextBtn.onclick = async () => { hideManualPlayModal(); await executeCardPlacement(chosenCol, mainGameWrapper); resolve(); }; }); return; } } } // --- Script Initialization --- let isScriptRunning = false; function startScript() { if (isScriptRunning) return; isScriptRunning = true; initializeAutoplayer(); } document.addEventListener("DOMContentLoaded", startScript); window.addEventListener("load", startScript); setTimeout(startScript, 1000); })();