/************************************************************
* Beccles u3a Raffle System
* Version: 1.2.0
* Author: Graham Evans (with Copilot assistance)
*
* This script powers the raffle draw interface on the Beccles u3a website.
* It is designed to be simple, transparent, and fair — and to produce
* a printable audit trail for exhibitions or fundraising events.
*
* This version includes:
* - Custom draw title input
* - Version number display
* - Full teaching-style comments for clarity
************************************************************/
// -----------------------------------------------------------
// GLOBAL VARIABLES
// -----------------------------------------------------------
// These store the state of the raffle system while the user interacts with it.
let bookCount = 0; // How many books have been added
let ticketPool = []; // The full list of all tickets across all books
let books = []; // Array of book objects: { code: "123456", tickets: [...] }
let lastWinners = []; // Stores the winners from the most recent draw
// Version number (displayed on the page)
const RAFFLE_VERSION = "1.2.0";
// -----------------------------------------------------------
// INITIALISATION: Display version number on page load
// -----------------------------------------------------------
document.addEventListener("DOMContentLoaded", () => {
const versionSpan = document.getElementById("raffleversion");
if (versionSpan) versionSpan.textContent = RAFFLE_VERSION;
});
// Add prize labels (1st, 2nd, 3rd, etc.)
// Converts 1 → "1st", 2 → "2nd", 3 → "3rd", etc.
function ordinal(n) {
const s = ["th", "st", "nd", "rd"], v = n % 100;
return n + (s[(v - 20) % 10] || s[v] || s[0]);
}
// -----------------------------------------------------------
// FUNCTION: addBookInput()
// PURPOSE: Adds a new book entry section to the page
// -----------------------------------------------------------
// Each book has:
// - A book code (e.g., 766766)
// - A start ticket number
// - An end ticket number
//
// These define the range of tickets in that book.
function addBookInput() {
bookCount++;
const container = document.getElementById("book-inputs");
// Create a new block of inputs for this book
const block = document.createElement("div");
block.style.margin = "1rem 0";
block.innerHTML = `
to
`;
container.appendChild(block);
}
// -----------------------------------------------------------
// FUNCTION: generatePool()
// PURPOSE: Builds the full list of tickets from all books
// -----------------------------------------------------------
// This function reads each book's code and ticket range,
// then generates ticket numbers like "766766-001", "766766-002", etc.
//
// The result is stored in ticketPool[] and books[].
function generatePool() {
ticketPool = [];
books = [];
for (let i = 1; i <= bookCount; i++) {
const bookCode = document.getElementById(`bookcode_${i}`).value.trim();
const start = parseInt(document.getElementById(`range_start_${i}`).value);
const end = parseInt(document.getElementById(`range_end_${i}`).value);
// Skip invalid entries
if (!bookCode || isNaN(start) || isNaN(end) || start > end) continue;
const bookTickets = [];
// Generate each ticket in the range
for (let t = start; t <= end; t++) {
const ticket = `${bookCode}-${String(t).padStart(3, '0')}`;
ticketPool.push(ticket);
bookTickets.push(ticket);
}
// Store book details
books.push({ code: bookCode, tickets: bookTickets });
}
// Display pool status
document.getElementById("pool-status").innerHTML =
`Ticket Pool Generated: ${ticketPool.length} tickets across ${books.length} books`;
}
// -----------------------------------------------------------
// FUNCTION: shuffle(array)
// PURPOSE: Randomises an array using Fisher-Yates shuffle
// -----------------------------------------------------------
// This ensures fair randomness when selecting winners.
function shuffle(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]];
}
}
// -----------------------------------------------------------
// FUNCTION: buildSummary()
// PURPOSE: Creates a summary of the draw for display and printing
// -----------------------------------------------------------
// This includes:
// - Draw title
// - Draw date
// - Book list
// - Total tickets
// - Fairness mode
// - Number of prizes
// - System version
function buildSummary() {
const fairness = document.querySelector('input[name="fairness"]:checked').value;
const actualPrizes = document.getElementById("actualprizes").value;
const title = document.getElementById("drawtitle").value || "Untitled Draw";
// Build book summary lines
let bookSummary = books.map(b => {
const start = parseInt(b.tickets[0].split("-")[1]);
const end = parseInt(b.tickets[b.tickets.length - 1].split("-")[1]);
return `${b.code} (${start}–${end})`;
}).join(" ");
return `
Total Tickets: ${ticketPool.length} Fairness Mode: ${fairness} Actual Prizes: ${actualPrizes} System Version: ${RAFFLE_VERSION}
`;
}
// -----------------------------------------------------------
// FUNCTION: runRaffleDraw()
// PURPOSE: Selects winners based on fairness mode
// -----------------------------------------------------------
// Fairness modes:
// - random: pure randomness across all tickets
// - equal: each book has equal chance regardless of size
// - weighted: larger books have proportionally more chance
//
// Test mode generates fake tickets for debugging.
function runRaffleDraw() {
const fairness = document.querySelector('input[name="fairness"]:checked').value;
const actualPrizes = parseInt(document.getElementById("actualprizes").value);
const testMode = document.getElementById("testmode").checked;
if (actualPrizes < 1) {
alert("Actual prizes must be at least 1.");
return;
}
let poolCopy;
// TEST MODE: Generates fake tickets for debugging
if (testMode) {
poolCopy = [];
for (let t = 1; t <= 1500; t++) {
poolCopy.push(`TEST-${String(t).padStart(4, '0')}`);
}
shuffle(poolCopy);
lastWinners = poolCopy.slice(0, actualPrizes);
}
// REAL MODE
else {
if (ticketPool.length === 0) {
document.getElementById("raffle-results").innerHTML =
"
Please generate the ticket pool first.
";
return;
}
// PURE RANDOMNESS
if (fairness === "random") {
poolCopy = [...ticketPool];
shuffle(poolCopy);
lastWinners = poolCopy.slice(0, actualPrizes);
}
// EQUAL CHANCE PER BOOK
else if (fairness === "equal") {
lastWinners = [];
for (let p = 0; p < actualPrizes; p++) {
const randomBook = books[Math.floor(Math.random() * books.length)];
const randomTicket = randomBook.tickets[Math.floor(Math.random() * randomBook.tickets.length)];
lastWinners.push(randomTicket);
}
}
// WEIGHTED FAIRNESS
else if (fairness === "weighted") {
lastWinners = [];
const weightedBooks = [];
// Larger books appear more often in weightedBooks[]
books.forEach(book => {
for (let i = 0; i < book.tickets.length; i++) {
weightedBooks.push(book);
}
});
for (let p = 0; p < actualPrizes; p++) {
const book = weightedBooks[Math.floor(Math.random() * weightedBooks.length)];
const ticket = book.tickets[Math.floor(Math.random() * book.tickets.length)];
lastWinners.push(ticket);
}
}
}
// Display results + summary
document.getElementById("raffle-results").innerHTML =
buildSummary() +
`
Winning Tickets
` +
lastWinners.map((w, i) => `
${ordinal(i+1)} Prize: ${w}
`).join("") +
"
";
}
// -----------------------------------------------------------
// FUNCTION: openPrintPage()
// PURPOSE: Opens a printable page showing winners + summary
// -----------------------------------------------------------
// This creates a new browser window containing:
// - Draw title
// - Draw date
// - Summary block
// - Large bordered winning tickets
//
// The page is formatted for landscape printing.
function openPrintPage() {
if (lastWinners.length === 0) {
alert("Please run the draw first.");
return;
}
const title = document.getElementById("drawtitle").value || "Untitled Draw";
const today = new Date();
const drawDate = today.toLocaleDateString("en-GB", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric"
});
const win = window.open("", "_blank");
win.document.write(`
${title}