Greasy Fork

来自缓存

Greasy Fork is available in English.

Neopets Scarab 21 Autoplayer

Automates Scarab 21.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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