// ==UserScript== // @name Neopets - Grarrl Keno Auto Player // @namespace https://scriptneo.com/ // @version 1.0.0 // @description Auto plays your Grarrl Keno. Supports Quick Pick (double click to finalize) or Random eggs (no duplicates, optional no-adjacent). Forces bet, waits for hatch, loops, persists settings, and logs prize_chart winners numbers one line per round once all 10 numbers exist. // @author You // @match *://*/*keno.phtml* // @run-at document-end // @grant GM_getValue // @grant GM_setValue // @downloadURL https://www.scriptneo.com/scripts/download.php?id=35 // @updateURL https://www.scriptneo.com/scripts/download.php?id=35 // ==/UserScript== (function () { "use strict"; const KEY_CFG = "gk_owner_cfg_v10"; const KEY_RUN = "gk_owner_run_v10"; const KEY_LOG = "gk_owner_winners_log_only_v4"; const DEFAULT_CFG = { betAmount: 500, pickMethod: "random", // "quickpick" or "random" randomEggCount: 10, randomEggMin: 1, randomEggMax: 80, noAdjacentNumbers: true, hatchWaitSeconds: 10, clickDelayMinMs: 120, clickDelayMaxMs: 320, quickPickFinalizeMinMs: 700, quickPickFinalizeMaxMs: 1400, maxRounds: 0, // 0 = unlimited stopOnError: true, verboseLog: true, autoResume: false, // winners-only log enableWinnersLog: true, maxLogLines: 1000, // New: make winners logging reliable even when tab is unfocused winnersExpectedCount: 10, winnersWaitTimeoutMs: 15000, winnersPollIntervalMs: 120 }; const DEFAULT_RUN = { running: false, rounds: 0, lastError: "", step: "idle", // idle | selection | waiting_results | results stepDueAt: 0, // Used to prevent double-logging lastWinnersLine: "" }; function loadObj(key, fallback) { const v = GM_getValue(key, null); if (!v || typeof v !== "object") return { ...fallback }; return { ...fallback, ...v }; } function saveObj(key, obj) { GM_setValue(key, obj); } function loadLogLines() { const v = GM_getValue(KEY_LOG, null); if (!Array.isArray(v)) return []; return v.filter((x) => typeof x === "string"); } function saveLogLines(lines) { GM_setValue(KEY_LOG, lines); } let cfg = loadObj(KEY_CFG, DEFAULT_CFG); let run = loadObj(KEY_RUN, DEFAULT_RUN); function saveCfg() { saveObj(KEY_CFG, cfg); } function saveRun() { saveObj(KEY_RUN, run); } function log(...args) { if (!cfg.verboseLog) return; console.log("[GK Auto]", ...args); } function warn(...args) { console.warn("[GK Auto]", ...args); } function errorLog(...args) { console.error("[GK Auto]", ...args); } function $all(sel, root = document) { return Array.from(root.querySelectorAll(sel)); } function clampInt(v, min, max, fallback) { const n = Number.parseInt(String(v), 10); if (!Number.isFinite(n)) return fallback; return Math.max(min, Math.min(max, n)); } function randInt(min, max) { const lo = Math.min(min, max); const hi = Math.max(min, max); return lo + Math.floor(Math.random() * (hi - lo + 1)); } function sleep(ms) { return new Promise((resolve) => window.setTimeout(resolve, ms)); } function setStatus(text) { const el = document.getElementById("gk_status_text"); if (el) el.textContent = text; run.lastError = ""; saveRun(); } function setError(text) { const el = document.getElementById("gk_status_text"); if (el) el.textContent = text; run.lastError = text; saveRun(); } function setRounds(n) { const el = document.getElementById("gk_rounds"); if (el) el.textContent = String(n); } function normalize(s) { return String(s || "").trim().toLowerCase(); } function findClickableByLabel(label) { const target = normalize(label); const inputs = $all('input[type="submit"], input[type="button"], input[type="reset"]'); for (const el of inputs) { if (normalize(el.value) === target) return el; } const buttons = $all("button"); for (const el of buttons) { if (normalize(el.textContent) === target) return el; } const roleBtns = $all('[role="button"]'); for (const el of roleBtns) { if (normalize(el.textContent) === target) return el; } return null; } function isSelectionScreen() { return !!findBetInput() && !!findClickableByLabel("Hatch those Eggs!"); } function isResultsScreen() { return !!findClickableByLabel("Play Again!"); } function findBetInput() { let el = document.querySelector('input[name="bet"]'); if (el) return el; try { const form = document.forms && document.forms.keno ? document.forms.keno : null; if (form && form.bet) return form.bet; } catch (e) { // ignore } el = document.querySelector('input[type="text"][maxlength="4"]'); return el || null; } function forceBet(amount) { const betInput = findBetInput(); if (!betInput) return false; const desired = String(clampInt(amount, 1, 999999, 500)); betInput.focus(); betInput.value = desired; betInput.dispatchEvent(new Event("input", { bubbles: true })); betInput.dispatchEvent(new Event("change", { bubbles: true })); betInput.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true, key: "0" })); try { if (typeof window.show_chart === "function") window.show_chart(); } catch (e) { // ignore } if (String(betInput.value || "").trim() !== desired) { betInput.value = desired; betInput.dispatchEvent(new Event("input", { bubbles: true })); betInput.dispatchEvent(new Event("change", { bubbles: true })); } return true; } async function clickWithDelay(el) { if (!el) return false; await sleep(randInt(cfg.clickDelayMinMs, cfg.clickDelayMaxMs)); el.click(); return true; } function stopRunning(reason) { run.running = false; run.step = "idle"; run.stepDueAt = 0; saveRun(); setStatus(reason || "Stopped."); updateButtons(); } function startRunning() { run.running = true; run.step = "selection"; run.stepDueAt = 0; saveRun(); setStatus("Running..."); updateButtons(); tick(); } function countMaxNoAdjacentPossible(min, max) { const lo = Math.min(min, max); const hi = Math.max(min, max); const L = hi - lo + 1; return Math.ceil(L / 2); } function buildUniqueRandomSetNoAdjacent(count, min, max) { const c = clampInt(count, 1, 999, 10); const lo = clampInt(min, 1, 1000000, 1); const hi = clampInt(max, lo, 1000000, 80); if (cfg.noAdjacentNumbers) { const maxPossible = countMaxNoAdjacentPossible(lo, hi); if (c > maxPossible) return null; } const picks = new Set(); const forbidden = new Set(); const MAX_ATTEMPTS = 5000; let attempts = 0; while (picks.size < c && attempts < MAX_ATTEMPTS) { attempts += 1; const n = randInt(lo, hi); if (picks.has(n)) continue; if (cfg.noAdjacentNumbers && forbidden.has(n)) continue; picks.add(n); if (cfg.noAdjacentNumbers) { forbidden.add(n - 1); forbidden.add(n + 1); } } if (picks.size !== c) return null; return Array.from(picks); } function findEggElement(n) { const byId = document.getElementById("ch" + String(n)); if (byId) return byId; const byName = document.querySelector('input[name="ch' + String(n) + '"]'); if (byName) return byName; const byValue = document.querySelector('input[type="checkbox"][value="' + String(n) + '"]'); if (byValue) return byValue; const byData = document.querySelector('[data-egg="' + String(n) + '"]'); if (byData) return byData; return null; } function isEggChecked(el) { if (!el) return false; if ("checked" in el) return !!el.checked; return el.getAttribute("aria-pressed") === "true" || el.classList.contains("selected"); } async function clearEggsIfPossible() { const clearBtn = findClickableByLabel("Clear All"); if (clearBtn) { setStatus("Clearing eggs..."); await clickWithDelay(clearBtn); await sleep(randInt(120, 260)); return true; } try { if (typeof window.reset_eggs === "function") { setStatus("Clearing eggs..."); window.reset_eggs(); await sleep(randInt(120, 260)); return true; } } catch (e) { // ignore } return false; } async function pickEggsRandomly(count, min, max) { const picks = buildUniqueRandomSetNoAdjacent(count, min, max); if (!picks) { throw new Error("Could not generate random eggs with the no-adjacent rule. Expand range or lower egg count."); } await clearEggsIfPossible(); setStatus("Picking eggs randomly..."); log("Picks:", picks.slice().sort((a, b) => a - b)); for (const n of picks) { const el = findEggElement(n); if (!el) throw new Error("Could not find egg element for #" + String(n)); if (!isEggChecked(el)) { await sleep(randInt(90, 180)); el.click(); } } await sleep(randInt(120, 260)); return true; } async function useQuickPick() { const qpBtn = findClickableByLabel("Quick Pick"); if (!qpBtn) throw new Error('Could not find "Quick Pick" button'); setStatus("Quick Pick starting..."); await clickWithDelay(qpBtn); const finalizeDelay = randInt(cfg.quickPickFinalizeMinMs, cfg.quickPickFinalizeMaxMs); setStatus("Quick Pick running..."); await sleep(finalizeDelay); setStatus("Quick Pick finalizing..."); await clickWithDelay(qpBtn); await sleep(randInt(150, 350)); return true; } function parseNumbersFromText(text) { const matches = String(text || "").match(/\b\d+\b/g); if (!matches) return []; return matches.map((x) => Number.parseInt(x, 10)).filter((n) => Number.isFinite(n)); } function extractWinnersOnlyNow() { const winnersDiv = document.getElementById("prize_chart"); if (!winnersDiv) return []; const winners = parseNumbersFromText(winnersDiv.textContent); return winners; } function winnersToLine(winners) { return winners.map((n) => String(n)).join(" "); } function appendWinnersLine(line) { if (!cfg.enableWinnersLog) return; const lines = loadLogLines(); lines.push(line); const cap = clampInt(cfg.maxLogLines, 10, 50000, DEFAULT_CFG.maxLogLines); while (lines.length > cap) lines.shift(); saveLogLines(lines); refreshLogUI(); } function refreshLogUI() { const box = document.getElementById("gk_log_box"); const countEl = document.getElementById("gk_log_count"); if (!box || !countEl) return; const lines = loadLogLines(); countEl.textContent = String(lines.length); box.value = lines.join("\n"); box.scrollTop = box.scrollHeight; } function getWinnersDiv() { return document.getElementById("prize_chart"); } function getFullWinnersIfReady() { const expected = clampInt(cfg.winnersExpectedCount, 1, 50, 10); const winners = extractWinnersOnlyNow(); if (winners.length < expected) return null; return winners.slice(0, expected); } async function waitForFullWinners() { const expected = clampInt(cfg.winnersExpectedCount, 1, 50, 10); const timeoutMs = clampInt(cfg.winnersWaitTimeoutMs, 1000, 60000, 15000); const pollMs = clampInt(cfg.winnersPollIntervalMs, 50, 1000, 120); const start = Date.now(); const immediate = getFullWinnersIfReady(); if (immediate) return immediate; const prizeChart = getWinnersDiv(); let observer = null; let done = false; const winnerPromise = new Promise((resolve) => { if (!prizeChart || typeof MutationObserver !== "function") { resolve(null); return; } observer = new MutationObserver(() => { if (done) return; const w = getFullWinnersIfReady(); if (w) { done = true; resolve(w); } }); observer.observe(prizeChart, { childList: true, subtree: true, characterData: true }); }); while (Date.now() - start < timeoutMs) { const got = getFullWinnersIfReady(); if (got) { done = true; if (observer) observer.disconnect(); return got; } const maybeFromObs = await Promise.race([winnerPromise, sleep(pollMs)]); if (Array.isArray(maybeFromObs) && maybeFromObs.length >= expected) { done = true; if (observer) observer.disconnect(); return maybeFromObs.slice(0, expected); } } done = true; if (observer) observer.disconnect(); return null; } async function tryLogWinnersIfNewReliable() { if (!cfg.enableWinnersLog) return false; const winners = await waitForFullWinners(); if (!winners || winners.length === 0) return false; const line = winnersToLine(winners); if (line === run.lastWinnersLine) return false; run.lastWinnersLine = line; saveRun(); appendWinnersLine(line); return true; } async function doSelectionStep() { if (!isSelectionScreen()) return false; if (!forceBet(cfg.betAmount)) throw new Error("Could not find bet input"); const hatchBtn = findClickableByLabel("Hatch those Eggs!"); if (!hatchBtn) throw new Error('Could not find "Hatch those Eggs!"'); await sleep(randInt(120, 260)); if (cfg.pickMethod === "quickpick") { await useQuickPick(); } else { const count = clampInt(cfg.randomEggCount, 2, 10, 10); const min = clampInt(cfg.randomEggMin, 1, 80, 1); const max = clampInt(cfg.randomEggMax, min, 80, 80); await pickEggsRandomly(count, min, max); } if (!forceBet(cfg.betAmount)) throw new Error("Could not find bet input"); run.step = "waiting_results"; run.stepDueAt = Date.now() + clampInt(cfg.hatchWaitSeconds, 1, 300, 10) * 1000; saveRun(); setStatus("Submitting Hatch..."); await clickWithDelay(hatchBtn); return true; } async function doWaitingResultsStep() { const due = run.stepDueAt || 0; const remainingMs = Math.max(0, due - Date.now()); if (remainingMs > 0) { setStatus("Waiting for hatch: " + String(Math.ceil(remainingMs / 1000)) + "s"); return true; } run.step = "results"; run.stepDueAt = 0; saveRun(); return true; } async function doResultsStep() { if (!isResultsScreen()) return false; setStatus("Collecting winners..."); const logged = await tryLogWinnersIfNewReliable(); if (logged) log("Logged winners:", run.lastWinnersLine); const playAgain = findClickableByLabel("Play Again!"); if (!playAgain) throw new Error('Could not find "Play Again!"'); run.rounds = clampInt(run.rounds, 0, 999999999, 0) + 1; saveRun(); setRounds(run.rounds); if (cfg.maxRounds > 0 && run.rounds >= cfg.maxRounds) { stopRunning("Max rounds reached (" + String(cfg.maxRounds) + ")."); return true; } // Allow next results to log even if it repeats later run.lastWinnersLine = ""; saveRun(); run.step = "selection"; run.stepDueAt = 0; saveRun(); setStatus('Clicking "Play Again!"...'); await clickWithDelay(playAgain); return true; } let ticking = false; async function tick() { if (ticking) return; ticking = true; try { if (!run.running) return; if (run.step === "selection") { const did = await doSelectionStep(); if (!did) setStatus("Waiting for selection screen..."); } else if (run.step === "waiting_results") { await doWaitingResultsStep(); } else if (run.step === "results") { const did = await doResultsStep(); if (!did) setStatus("Waiting for results screen..."); } else { run.step = isSelectionScreen() ? "selection" : (isResultsScreen() ? "results" : "selection"); saveRun(); } } catch (e) { const msg = e && e.message ? e.message : "Unknown error"; errorLog("Tick error:", e); setError("Error: " + msg); if (cfg.stopOnError) { stopRunning("Stopped due to error."); } else { warn("Continuing after error because stopOnError=false"); } } finally { ticking = false; if (run.running) window.setTimeout(() => tick(), 250); } } async function copyTextToClipboard(text) { const t = String(text || ""); try { if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") { await navigator.clipboard.writeText(t); return true; } } catch (e) { // ignore } const ta = document.createElement("textarea"); ta.value = t; ta.style.position = "fixed"; ta.style.left = "-9999px"; ta.style.top = "0"; document.body.appendChild(ta); ta.focus(); ta.select(); let ok = false; try { ok = document.execCommand("copy"); } catch (e) { ok = false; } document.body.removeChild(ta); return ok; } function updateButtons() { const startBtn = document.getElementById("gk_start"); const stopBtn = document.getElementById("gk_stop"); const onceBtn = document.getElementById("gk_once"); if (startBtn) startBtn.disabled = run.running; if (onceBtn) onceBtn.disabled = run.running; if (stopBtn) stopBtn.disabled = !run.running; } function buildUI() { const panel = document.createElement("div"); panel.id = "gk_panel"; panel.style.position = "fixed"; panel.style.right = "12px"; panel.style.bottom = "12px"; panel.style.zIndex = "999999"; panel.style.width = "440px"; panel.style.background = "rgba(0,0,0,0.86)"; panel.style.color = "#fff"; panel.style.borderRadius = "12px"; panel.style.padding = "10px"; panel.style.font = "13px/1.35 Arial, sans-serif"; panel.style.boxShadow = "0 10px 26px rgba(0,0,0,0.35)"; panel.innerHTML = `