/************************************************************ * 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 `

Raffle Summary

Draw Title: ${title}
Draw Date: ${new Date().toLocaleDateString("en-GB", { weekday: "long", year: "numeric", month: "long", day: "numeric" })}

Books Entered:
${bookSummary}

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

"; } // ----------------------------------------------------------- // 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}

${title}

Raffle Prize Winners

Draw Date: ${drawDate}

${buildSummary()} ${lastWinners.map((w, i) => `
${ordinal(i+1)} Prize
${w}
`).join("")} `); win.document.close(); win.print(); }