Neopets: Lottery Numbers Generator

Generates not-so-random numbers for you to play in the lottery, making sure you repeat as little numbers as possible to increase your chances of winning. And it actually works! Unlike 90% of the lottery userscripts!

// ==UserScript==
// @name         Neopets: Lottery Numbers Generator
// @namespace    https://github.com/saahphire/NeopetsUserscripts
// @version      1.0.0
// @description  Generates not-so-random numbers for you to play in the lottery, making sure you repeat as little numbers as possible to increase your chances of winning. And it actually works! Unlike 90% of the lottery userscripts!
// @author       saahphire
// @homepageURL  https://github.com/saahphire/NeopetsUserscripts
// @homepage     https://github.com/saahphire/NeopetsUserscripts
// @match        *://*.neopets.com/games/lottery.phtml
// @icon         https://www.google.com/s2/favicons?sz=64&domain=neopets.com
// @license      Unlicense
// ==/UserScript==

/*
•:•.•:•.•:•:•:•:•:•:•:••:•.•:•.•:•:•:•:•:•:•:•:•.•:•.•:•:•:•:•:•:•:••:•.•:•.•:•.•:•:•:•:•:•:•:•:•.•:•:•.•:•.••:•.•:•.••:
........................................................................................................................
☆ ⠂⠄⠄⠂⠁⠁⠂⠄⠄⠂✦ ⠂⠄⠄⠂⠁⠁⠂⠄⠄⠂☆ ⠂⠄⠄⠂⠁⠁⠂⠄⠄⠂✦ ⠂⠄⠄⠂⠁⠁⠂⠄⠂⠄⠄⠂☆ ⠂⠄⠄⠂⠁⠁⠂⠄⠄⠂✦ ⠂⠄⠄⠂⠁⠁⠂⠄⠂⠄⠄⠂☆ ⠂⠄⠄⠂⠁⠁⠂⠄⠄⠂✦
    This script works as of October 19th, 2025. It doesn't use GM3 and does the following:
    - Generates numbers for you to play in the lottery
    - Ensures these numbers are only repeated the minimum possible amount of times between different tickets
    - Buys and generates at the same time
    - Adds a button for you to buy your lucky numbers instead of random ones. The button disappears after you buy them.
    - Adds both buttons to a fixed location so you don't have to move your mouse between tickets
    - Makes both buttons unclickable while the server is processing your request so it doesn't give you an error page

    I tried making my own script but gave up in the middle. Math hurts my head.
    The idea of interleaving numbers is from https://www.andrew.cmu.edu/user/kmliu/neopets/lottery2.html

    ✦ ⌇ saahphire
☆ ⠂⠄⠄⠂⠁⠁⠂⠄⠄⠂✦ ⠂⠄⠄⠂⠁⠁⠂⠄⠄⠂☆ ⠂⠄⠄⠂⠁⠁⠂⠄⠄⠂✦ ⠂⠄⠄⠂⠁⠁⠂⠄⠂⠄⠄⠂☆ ⠂⠄⠄⠂⠁⠁⠂⠄⠄⠂✦ ⠂⠄⠄⠂⠁⠁⠂⠄⠂⠄⠄⠂☆ ⠂⠄⠄⠂⠁⠁⠂⠄⠄⠂✦
........................................................................................................................
•:•.•:•.•:•:•:•:•:•:•:••:•.•:•.•:•:•:•:•:•:•:•:•.•:•.•:•:•:•:•:•:•:••:•.•:•.•:•.•:•:•:•:•:•:•:•:•.•:•:•.•:•.••:•.•:•.••:
*/

// Add your lucky numbers here! If you'd like to get rid of the lucky button instead, change "true" to "false"
const enableLuckyButton = true;
const luckyNumbers = [1, 2, 3, 4, 5, 6];
const verbose = true;

const inputNames = ["one", "two", "three", "four", "five", "six"];

const interleave = (array) => {
    const res = [];
    const a = array.slice(0, Math.floor(array.length / 2));
    const b = array.slice(Math.ceil(array.length / 2));
    for(var i = 0; i < a.length; i++) {
        res[i * 2] = a[i]; 
        res[i * 2 + 1] = b[i]; 
    }
    return res;
}

const fisherYatesShuffle = (array) => {
    for (let i = array.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [array[i], array[j]] = [array[j], array[i]];
    }
    return array;
}

const interleaveNTimes = (array, n) => {
    if(n === 0) return array;
    return interleaveNTimes(interleave(array), n - 1);
}

const getCurrentNumbers = () => [...document.querySelector(".content p:nth-of-type(3)").childNodes]
                                .filter((e, i) => i % 4 === 2)
                                .flatMap(txt => txt.textContent.match(/\d+/g).map(num => parseInt(num)))
                                .sort((a, b) => a - b);

const getRemoveFrom = (currentNumbers) => {
    return currentNumbers
    .filter((number, index) => currentNumbers.findIndex(n => n === number) === index)
    .map(number => [number, Array.from(new Array(Math.min(currentNumbers.filter(n => n === number).length, 4)), (e, i) => fisherYatesShuffle([0, 1, 2, 3])[i])]);
}

const isValidArray = (array, previousTicketNumbers) => {
    if(array.some((number, index) => array.findIndex(n => n === number) !== index)) return false;
    for(let i = 0; i < previousTicketNumbers.length; i += 6)
        if(previousTicketNumbers.slice(i, i + 6).every(number => array.findIndex(n => n === number) !== -1)) return false;
    return true;
}

const createTicketNumbers = (currentNumbers, attempts = 0) => {
    if(attempts > 250) throw new Error("Too many shuffling attempts!");
    const removeFrom = getRemoveFrom(currentNumbers);
    const numbers = Array.from(new Array(4), () => Array.from(new Array(30), (_, i) => i + 1));
    removeFrom.forEach(([number, arrays]) => arrays.forEach(array => numbers[array] = numbers[array].slice(0, number - 1).concat(numbers[array].slice(number))));
    let interleaved = Array.from(new Array(4), () => []);
    for(let i = 0; i < 3; i++)
        interleaved[i] = interleaveNTimes(fisherYatesShuffle(numbers[i]), i * 2);
    const result = interleaved.flat();
    if(isValidArray(result.slice(0, 6), currentNumbers)) return result;
    return createTicketNumbers(currentNumbers, attempts + 1);
}

const getRandomTicketNumbers = (currentNumbers) => {
    if(currentNumbers.length === 6) return currentNumbers;
    const newNumber = Math.ceil(Math.random() * 30);
    return getRandomTicketNumbers(currentNumbers.includes(newNumber) ? currentNumbers : currentNumbers.concat([newNumber]));
}

const fillTicket = (numbers) => {
    const sorted = numbers.sort((a, b) => a - b);
    inputNames.forEach((name, index) => {
        document.querySelector(`input[name="${name}"]`).value = sorted[index];
    });
}

const fillLotteryNumbers = () => {
    const currentNumbers = getCurrentNumbers();
    if(currentNumbers.length >= 120) return false;
    try {
        const numbers = createTicketNumbers(currentNumbers);
        if(verbose) console.log(`Generated ticket numbers (only the first six will be used): [${numbers.join(', ')}]`);
        fillTicket(numbers.slice(0, 6));
    } catch (e) {
        console.error(e);
        document.querySelector(".content p:nth-of-type(2)").insertAdjacentHTML("beforeBegin", `<p style="color:darkred">Something went wrong! You can buy non-optimized numbers like usual or refresh the page.</p><p style="color:red;">${e}</p>`);
        fillTicket(getRandomTicketNumbers(currentNumbers));
    }
    return true;
}

const addLuckyButton = (form) => {
    if(!enableLuckyButton) return;
    const orderedLucky = luckyNumbers.sort((a, b) => a - b);
    const alreadyPlayed = [...document.querySelector(".content p:nth-of-type(3)").childNodes]
        .filter((e, i) => i % 4 === 2).
        some(t => t.textContent.replace(":", "").trim() === orderedLucky.join(" "));
    if(alreadyPlayed) return;
    const luckyButton = document.createElement("input");
    luckyButton.type = "button";
    luckyButton.value = "Buy Your Lucky Ticket!";
    luckyButton.style = "padding: 1em;"
    document.querySelector('input[form="lottery"]').insertAdjacentElement("afterEnd", luckyButton);
    luckyButton.addEventListener("click", () => {
        luckyNumbers.forEach((n, i) => document.querySelector(`input[name="${inputNames[i]}"]`).value = n);
        form.submit();
    });
    return luckyButton;
}

const moveButton = (form) => {
    const button = form.querySelector('input[type="submit"]');
    document.querySelector(".content p:nth-of-type(2)").insertAdjacentElement("beforeBegin", button);
    form.id = "lottery";
    button.setAttribute("form", "lottery");
    button.style = "padding: 1em;";
    return button;
}

const changeHTML = (canBuy) => {
    const form = document.querySelector('form[action="process_lottery.phtml"]');
    const buyButton = moveButton(form);
    if(canBuy) {
        const luckyButton = addLuckyButton(form);
        form.addEventListener("submit", () => {
            buyButton.disabled = true;
            luckyButton.disabled = true;
        })
    }
    else {
        buyButton.disabled = true;
        buyButton.value = "All 20 tickets have been bought!"
        document.querySelectorAll('input[size="2"]').forEach(input => input.disabled = true);
    }
}

(function() {
    'use strict';
    const canBuy = fillLotteryNumbers();
    changeHTML(canBuy);
})();