Games By web crawler 0 installs Rating 0.0 (0) approved

Neopets - Grarrl Keno Auto Player

Auto plays your Grarrl Keno. Supports Quick Pick (double click to finalize) or Random eggs (no duplicates, optional no-adjacent).
auto-player grarrl keno neopets
https://www.scriptneo.com/script/neopets-grarrl-keno-auto-player

Version selector


SHA256
65041e660ff10d653a8e4a9baf78fa03eb49d7ca7101e7150256b572eb78b175
Static scan
Score: 1
Flags: uses_clipboard

Source code

// ==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 = `
      <div style="display:flex; align-items:center; justify-content:space-between; gap:10px;">
        <div style="font-weight:700;">Grarrl Keno Auto</div>
        <button type="button" id="gk_toggle" style="cursor:pointer; border:0; border-radius:10px; padding:5px 9px;">Hide</button>
      </div>

      <div id="gk_body" style="margin-top:10px;">
        <div style="display:flex; gap:8px; margin-bottom:8px;">
          <button type="button" id="gk_start" style="flex:1; cursor:pointer; border:0; border-radius:12px; padding:10px; font-weight:700;">Start</button>
          <button type="button" id="gk_stop" style="width:90px; cursor:pointer; border:0; border-radius:12px; padding:10px; font-weight:700;">Stop</button>
        </div>

        <div style="display:flex; gap:8px; margin-bottom:10px;">
          <button type="button" id="gk_once" style="flex:1; cursor:pointer; border:0; border-radius:12px; padding:10px; font-weight:700;">Run once</button>
          <div style="width:90px; display:flex; align-items:center; justify-content:center; background:rgba(255,255,255,0.1); border-radius:12px; padding:10px;">
            <div style="text-align:center;">
              <div style="font-size:11px; opacity:0.85;">Rounds</div>
              <div id="gk_rounds" style="font-weight:800;">0</div>
            </div>
          </div>
        </div>

        <div style="background:rgba(255,255,255,0.08); border-radius:12px; padding:10px; margin-bottom:10px;">
          <div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
            <label style="width:210px;">Bet (NP)</label>
            <input id="gk_bet" type="number" min="1" max="999999" step="1" style="flex:1; border:0; border-radius:10px; padding:8px 10px;" />
          </div>

          <div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
            <label style="width:210px;">Pick method</label>
            <select id="gk_pick" style="flex:1; border:0; border-radius:10px; padding:8px 10px;">
              <option value="quickpick">Quick Pick</option>
              <option value="random">Random eggs</option>
            </select>
          </div>

          <div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
            <label style="width:210px;">No adjacent numbers</label>
            <select id="gk_noadj" style="flex:1; border:0; border-radius:10px; padding:8px 10px;">
              <option value="yes">Yes</option>
              <option value="no">No</option>
            </select>
          </div>

          <div id="gk_random_box" style="background:rgba(255,255,255,0.06); border-radius:10px; padding:10px; margin-bottom:8px;">
            <div style="font-weight:700; margin-bottom:8px;">Random egg settings</div>
            <div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
              <label style="width:210px;">Egg count</label>
              <input id="gk_rand_count" type="number" min="2" max="10" step="1" style="flex:1; border:0; border-radius:10px; padding:8px 10px;" />
            </div>
            <div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
              <label style="width:210px;">Egg min</label>
              <input id="gk_rand_min" type="number" min="1" max="80" step="1" style="flex:1; border:0; border-radius:10px; padding:8px 10px;" />
            </div>
            <div style="display:flex; gap:10px; align-items:center;">
              <label style="width:210px;">Egg max</label>
              <input id="gk_rand_max" type="number" min="1" max="80" step="1" style="flex:1; border:0; border-radius:10px; padding:8px 10px;" />
            </div>
          </div>

          <div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
            <label style="width:210px;">Wait for hatch (sec)</label>
            <input id="gk_wait" type="number" min="1" max="300" step="1" style="flex:1; border:0; border-radius:10px; padding:8px 10px;" />
          </div>

          <div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
            <label style="width:210px;">Click delay min (ms)</label>
            <input id="gk_cd_min" type="number" min="0" max="5000" step="1" style="flex:1; border:0; border-radius:10px; padding:8px 10px;" />
          </div>

          <div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
            <label style="width:210px;">Click delay max (ms)</label>
            <input id="gk_cd_max" type="number" min="0" max="5000" step="1" style="flex:1; border:0; border-radius:10px; padding:8px 10px;" />
          </div>

          <div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
            <label style="width:210px;">QuickPick finalize min (ms)</label>
            <input id="gk_qp_min" type="number" min="0" max="10000" step="1" style="flex:1; border:0; border-radius:10px; padding:8px 10px;" />
          </div>

          <div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
            <label style="width:210px;">QuickPick finalize max (ms)</label>
            <input id="gk_qp_max" type="number" min="0" max="10000" step="1" style="flex:1; border:0; border-radius:10px; padding:8px 10px;" />
          </div>

          <div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
            <label style="width:210px;">Max rounds (0 = infinite)</label>
            <input id="gk_max_rounds" type="number" min="0" max="1000000" step="1" style="flex:1; border:0; border-radius:10px; padding:8px 10px;" />
          </div>

          <div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
            <label style="width:210px;">Stop on error</label>
            <select id="gk_stoperr" style="flex:1; border:0; border-radius:10px; padding:8px 10px;">
              <option value="yes">Yes</option>
              <option value="no">No</option>
            </select>
          </div>

          <div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
            <label style="width:210px;">Verbose log</label>
            <select id="gk_verbose" style="flex:1; border:0; border-radius:10px; padding:8px 10px;">
              <option value="yes">Yes</option>
              <option value="no">No</option>
            </select>
          </div>

          <div style="display:flex; gap:10px; align-items:center; margin-bottom:8px;">
            <label style="width:210px;">Auto resume after refresh</label>
            <select id="gk_resume" style="flex:1; border:0; border-radius:10px; padding:8px 10px;">
              <option value="no">No</option>
              <option value="yes">Yes</option>
            </select>
          </div>

          <div style="display:flex; gap:10px; align-items:center;">
            <label style="width:210px;">Max log lines</label>
            <input id="gk_log_max" type="number" min="10" max="50000" step="1" style="flex:1; border:0; border-radius:10px; padding:8px 10px;" />
          </div>

          <div style="display:flex; gap:8px; margin-top:10px;">
            <button type="button" id="gk_save" style="flex:1; cursor:pointer; border:0; border-radius:12px; padding:10px; font-weight:700;">Save settings</button>
            <button type="button" id="gk_reset" style="width:120px; cursor:pointer; border:0; border-radius:12px; padding:10px; font-weight:700;">Reset</button>
          </div>
        </div>

        <div style="background:rgba(255,255,255,0.08); border-radius:12px; padding:10px; margin-bottom:10px;">
          <div style="display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:8px;">
            <div style="font-weight:700;">Winners log <span style="opacity:0.85;">(lines: <span id="gk_log_count">0</span>)</span></div>
            <div style="display:flex; gap:8px;">
              <button type="button" id="gk_log_copy" style="cursor:pointer; border:0; border-radius:10px; padding:6px 10px; font-weight:700;">Copy</button>
              <button type="button" id="gk_log_clear" style="cursor:pointer; border:0; border-radius:10px; padding:6px 10px; font-weight:700;">Clear</button>
            </div>
          </div>
          <textarea id="gk_log_box" readonly style="width:100%; height:150px; border:0; border-radius:10px; padding:10px; resize:vertical;"></textarea>
        </div>

        <div style="padding:10px; background:rgba(255,255,255,0.08); border-radius:12px;">
          Status: <span id="gk_status_text">Ready</span>
        </div>
      </div>
    `;

    document.body.appendChild(panel);

    const toggleBtn = document.getElementById("gk_toggle");
    const body = document.getElementById("gk_body");
    toggleBtn.addEventListener("click", () => {
      const hidden = body.style.display === "none";
      body.style.display = hidden ? "block" : "none";
      toggleBtn.textContent = hidden ? "Hide" : "Show";
    });

    const betEl = document.getElementById("gk_bet");
    const pickEl = document.getElementById("gk_pick");
    const noAdjEl = document.getElementById("gk_noadj");
    const randBox = document.getElementById("gk_random_box");
    const randCountEl = document.getElementById("gk_rand_count");
    const randMinEl = document.getElementById("gk_rand_min");
    const randMaxEl = document.getElementById("gk_rand_max");
    const waitEl = document.getElementById("gk_wait");
    const cdMinEl = document.getElementById("gk_cd_min");
    const cdMaxEl = document.getElementById("gk_cd_max");
    const qpMinEl = document.getElementById("gk_qp_min");
    const qpMaxEl = document.getElementById("gk_qp_max");
    const maxRoundsEl = document.getElementById("gk_max_rounds");
    const stopErrEl = document.getElementById("gk_stoperr");
    const verboseEl = document.getElementById("gk_verbose");
    const resumeEl = document.getElementById("gk_resume");
    const logMaxEl = document.getElementById("gk_log_max");

    betEl.value = String(cfg.betAmount);
    pickEl.value = cfg.pickMethod === "quickpick" ? "quickpick" : "random";
    noAdjEl.value = cfg.noAdjacentNumbers ? "yes" : "no";
    randCountEl.value = String(cfg.randomEggCount);
    randMinEl.value = String(cfg.randomEggMin);
    randMaxEl.value = String(cfg.randomEggMax);
    waitEl.value = String(cfg.hatchWaitSeconds);
    cdMinEl.value = String(cfg.clickDelayMinMs);
    cdMaxEl.value = String(cfg.clickDelayMaxMs);
    qpMinEl.value = String(cfg.quickPickFinalizeMinMs);
    qpMaxEl.value = String(cfg.quickPickFinalizeMaxMs);
    maxRoundsEl.value = String(cfg.maxRounds);
    stopErrEl.value = cfg.stopOnError ? "yes" : "no";
    verboseEl.value = cfg.verboseLog ? "yes" : "no";
    resumeEl.value = cfg.autoResume ? "yes" : "no";
    logMaxEl.value = String(cfg.maxLogLines);

    function syncRandomBoxVisibility() {
      randBox.style.display = pickEl.value === "random" ? "block" : "none";
    }
    pickEl.addEventListener("change", syncRandomBoxVisibility);
    syncRandomBoxVisibility();

    setRounds(run.rounds);
    refreshLogUI();
    updateButtons();

    document.getElementById("gk_start").addEventListener("click", () => startRunning());
    document.getElementById("gk_stop").addEventListener("click", () => stopRunning("Stopped by you."));
    document.getElementById("gk_once").addEventListener("click", async () => {
      const wasRunning = run.running;
      run.running = false;
      saveRun();

      try {
        if (isSelectionScreen()) {
          setStatus("Running once...");
          await doSelectionStep();
          setStatus("Submitted once. Click Start to keep looping.");
        } else if (isResultsScreen()) {
          setStatus("Running once...");
          await doResultsStep();
          setStatus("Clicked Play Again once.");
        } else {
          setStatus("Page not recognized for Run once.");
        }
      } catch (e) {
        setError("Error: " + (e && e.message ? e.message : "Unknown error"));
      } finally {
        run.running = wasRunning;
        saveRun();
        updateButtons();
      }
    });

    document.getElementById("gk_save").addEventListener("click", () => {
      cfg.betAmount = clampInt(betEl.value, 1, 999999, DEFAULT_CFG.betAmount);
      cfg.pickMethod = pickEl.value === "quickpick" ? "quickpick" : "random";
      cfg.noAdjacentNumbers = noAdjEl.value === "yes";

      cfg.randomEggCount = clampInt(randCountEl.value, 2, 10, DEFAULT_CFG.randomEggCount);
      cfg.randomEggMin = clampInt(randMinEl.value, 1, 80, DEFAULT_CFG.randomEggMin);
      cfg.randomEggMax = clampInt(randMaxEl.value, cfg.randomEggMin, 80, DEFAULT_CFG.randomEggMax);

      cfg.hatchWaitSeconds = clampInt(waitEl.value, 1, 300, DEFAULT_CFG.hatchWaitSeconds);

      cfg.clickDelayMinMs = clampInt(cdMinEl.value, 0, 5000, DEFAULT_CFG.clickDelayMinMs);
      cfg.clickDelayMaxMs = clampInt(cdMaxEl.value, 0, 5000, DEFAULT_CFG.clickDelayMaxMs);
      if (cfg.clickDelayMaxMs < cfg.clickDelayMinMs) {
        const tmp = cfg.clickDelayMinMs;
        cfg.clickDelayMinMs = cfg.clickDelayMaxMs;
        cfg.clickDelayMaxMs = tmp;
      }

      cfg.quickPickFinalizeMinMs = clampInt(qpMinEl.value, 0, 10000, DEFAULT_CFG.quickPickFinalizeMinMs);
      cfg.quickPickFinalizeMaxMs = clampInt(qpMaxEl.value, 0, 10000, DEFAULT_CFG.quickPickFinalizeMaxMs);
      if (cfg.quickPickFinalizeMaxMs < cfg.quickPickFinalizeMinMs) {
        const tmp2 = cfg.quickPickFinalizeMinMs;
        cfg.quickPickFinalizeMinMs = cfg.quickPickFinalizeMaxMs;
        cfg.quickPickFinalizeMaxMs = tmp2;
      }

      cfg.maxRounds = clampInt(maxRoundsEl.value, 0, 1000000, DEFAULT_CFG.maxRounds);
      cfg.stopOnError = stopErrEl.value === "yes";
      cfg.verboseLog = verboseEl.value === "yes";
      cfg.autoResume = resumeEl.value === "yes";

      cfg.maxLogLines = clampInt(logMaxEl.value, 10, 50000, DEFAULT_CFG.maxLogLines);

      saveCfg();

      if (isSelectionScreen()) forceBet(cfg.betAmount);

      setStatus("Settings saved.");
      refreshLogUI();
      updateButtons();
    });

    document.getElementById("gk_reset").addEventListener("click", () => {
      cfg = { ...DEFAULT_CFG };
      saveCfg();

      betEl.value = String(cfg.betAmount);
      pickEl.value = cfg.pickMethod;
      noAdjEl.value = cfg.noAdjacentNumbers ? "yes" : "no";
      randCountEl.value = String(cfg.randomEggCount);
      randMinEl.value = String(cfg.randomEggMin);
      randMaxEl.value = String(cfg.randomEggMax);
      waitEl.value = String(cfg.hatchWaitSeconds);
      cdMinEl.value = String(cfg.clickDelayMinMs);
      cdMaxEl.value = String(cfg.clickDelayMaxMs);
      qpMinEl.value = String(cfg.quickPickFinalizeMinMs);
      qpMaxEl.value = String(cfg.quickPickFinalizeMaxMs);
      maxRoundsEl.value = String(cfg.maxRounds);
      stopErrEl.value = cfg.stopOnError ? "yes" : "no";
      verboseEl.value = cfg.verboseLog ? "yes" : "no";
      resumeEl.value = cfg.autoResume ? "yes" : "no";
      logMaxEl.value = String(cfg.maxLogLines);

      syncRandomBoxVisibility();
      setStatus("Settings reset.");
      refreshLogUI();
    });

    document.getElementById("gk_log_copy").addEventListener("click", async () => {
      const lines = loadLogLines().join("\n");
      const ok = await copyTextToClipboard(lines);
      setStatus(ok ? "Log copied to clipboard." : "Copy failed in this browser.");
    });

    document.getElementById("gk_log_clear").addEventListener("click", () => {
      saveLogLines([]);
      run.lastWinnersLine = "";
      saveRun();
      refreshLogUI();
      setStatus("Log cleared.");
    });
  }

  buildUI();

  if (isSelectionScreen()) forceBet(cfg.betAmount);

  // If page loads already showing results, try to log once fully ready
  (async () => {
    if (document.getElementById("prize_chart")) {
      await tryLogWinnersIfNewReliable();
      refreshLogUI();
    }
  })();

  if (run.running && cfg.autoResume) {
    setStatus("Auto resume active. Continuing...");
    updateButtons();
    tick();
  } else if (run.running && !cfg.autoResume) {
    stopRunning("Page refreshed. Auto resume is off, so it stopped.");
  } else {
    setStatus("Ready. Click Start to begin looping.");
  }
})();