import { Account } from "~/shared/types/account"; import { Role, Schedule, Shift, ShiftSlot, TimeSlot } from "~/shared/types/schedule"; import { toId } from "~/shared/utils/functions"; const locations = [ { name: "Stage", description: "Inside the main building." }, { name: "Clubhouse", description: "That big red building in the middle of the park." }, { name: "Summerhouse", description: "Next to the campfire by the lake" }, { name: "Campfire", description: "Next to the big tree by the lake." }, { name: "Outside", description: "Takes place somewhere outside." } ] const events = [ { name: "Arcade", description: "Play retro games!", slots: [ "d1 12:00 4h clubhouse", "d2 12:00 4h clubhouse", "d3 12:00 4h clubhouse", ], }, { name: "Arcade Setup", crew: true, slots: ["d1 11:00 1h clubhouse"], }, { name: "Bonfire Stories", description: "Share your stories as we sit cosily around the campfire.", slots: ["d2 18:00 2h campfire"], }, { name: "Stage Rigging", crew: true, slots: ["d1 11:00 7h30m stage"]}, { name: "Reconfigure for DJ", crew: true, slots: [ "d2 19:00 1h stage", "d3 18:45 1h15m stage", ], }, { name: "DJ Alpha", slots: ["d2 20:00 2h stage"] }, { name: "DJ Bravo", slots: ["d3 20:00 2h stage"] }, { name: "DJ Charlie", slots: ["d3 22:00 2h stage"] }, { name: "Prepare Fursuit Games", crew: true, slots: ["d4 17:00 1h clubhouse"] }, { name: "Fursuit Games", description: "Playful time for the suiters.", slots: ["d4 18:00 2h clubhouse"], }, { name: "Fishing Trip", slots: ["d3 12:00 3h30m outside"]}, { name: "Opening", slots: ["d1 18:30 1h30m stage"]}, { name: "Closing", slots: ["d5 10:00 1h stage"]}, { name: "Stage Teardown", crew: true, slots: ["d5 11:00 4h stage"]}, { name: "Setup Board Games", crew: true, slots: ["d1 11:30 30m summerhouse"]}, { name: "Board Games", slots: [ "d1 12:00 4h summerhouse", "d2 12:00 4h summerhouse", "d3 12:00 4h summerhouse", "d4 12:00 4h summerhouse", ], }, { name: "Teardown Board Games", crew: true, slots: ["d4 16:00 30m summerhouse"]}, { name: "📷meet", slots: ["d3 19:00 1h10m summerhouse"]}, { name: "Prepare Karaoke", crew: true, slots: [ "d3 20:00 1h clubhouse", "d4 20:00 1h clubhouse", ], }, { name: "Karaoke", slots: [ "d3 21:00 2h clubhouse", "d4 21:00 2h clubhouse", ], }, { name: "Dance", slots: ["d1 20:00 3h stage"]}, { name: "Prepare Charity Auction", crew: true, slots: ["d4 10:00 3h stage"]}, { name: "Charity Auction", slots: ["d4 13:00 2h stage"]}, { name: "Tournament Preparation", crew: true, slots: ["d2 15:00 2h stage"]}, { name: "Tournament", slots: ["d2 17:00 2h stage"]}, { name: "Canoe Trip", slots: ["d4 11:00 4h30m outside"]}, { name: "Dinner Preparations", crew: true, slots: [ "d1 14:00 2h campfire", "d2 14:00 2h campfire", "d3 14:00 2h campfire", "d4 14:00 2h campfire", ], }, { name: "Dinner", slots: [ "d1 16:00 1h campfire", "d2 16:00 1h campfire", "d3 16:00 1h campfire", "d4 16:00 1h campfire", ], }, { name: "Dinner Cleanup", crew: true, slots: [ "d1 17:00 1h campfire", "d2 17:00 1h campfire", "d3 17:00 1h campfire", "d4 17:00 1h campfire", ], }, { name: "Prepare Film Night", crew: true, slots: ["d4 19:00 2h stage"]}, { name: "Film Night", slots: ["d4 21:00 2h stage"]}, { name: "Prepare Group Photo", crew: true, slots: ["d3 17:00 1h stage"]}, { name: "Group Photo", slots: ["d3 18:00 45m stage"]}, { name: "Setup Artist Alley", crew: true, slots: ["d4 10:00 2h clubhouse"]}, { name: "Artist Alley", slots: ["d4 12:00 4h clubhouse"]}, { name: "Teardown Artist Alley", crew: true, slots: ["d4 16:00 1h clubhouse"]}, { name: "Feedback Panel", slots: ["d5 12:00 1h clubhouse"]}, ]; const roles: Role[] = [ { id: "medic", name: "Medic" }, { id: "security", name: "Security" }, ] const rota = [ { name: "Medic Early", role: "medic", slots: [ "d1 12:00 4h", "d2 12:00 4h", "d3 12:00 4h", "d4 11:00 5h", "d5 10:00 3h", ], }, { name: "Medic Late", role: "medic", slots: [ "d1 16:00 7h", "d2 16:00 6h", "d3 16:00 8h", "d4 16:00 7h", ], }, { name: "Security Early", role: "security", slots: [ "d1 12:00 6h", "d2 12:00 6h", "d3 12:00 6h", "d4 11:00 7h", "d5 10:00 3h", ], }, { name: "Security Late", role: "security", slots: [ "d1 18:00 5h", "d2 18:00 4h", "d3 18:00 6h", "d4 18:00 5h", ], }, ] function toIso(date: Date) { return date.toISOString().replace(":00.000Z", "Z"); } function toDates(origin: Date, day: string, start: string, duration: string) { const [startHours, startMinutes] = start.split(":").map(time => parseInt(time, 10)); const dayNumber = parseInt(day.slice(1)); const startDate = new Date(origin); startDate.setUTCDate(startDate.getUTCDate() + dayNumber); startDate.setUTCHours(startDate.getUTCHours() + startHours); startDate.setUTCMinutes(startDate.getUTCMinutes() + startMinutes); const [_, durationHours, durationMinutes] = /(?:(\d+)h)?(?:(\d+)m)?/.exec(duration)!; const durationTotal = parseInt(durationHours ?? "0") * 60 + parseInt(durationMinutes ?? "0") const endDate = new Date(startDate.getTime() + durationTotal * 60e3); return [startDate, endDate]; } function toSlot(origin: Date, id: string, shorthand: string, index: number, counts: Map, idToAssigned: Map): TimeSlot { const [day, start, duration, location] = shorthand.split(" "); const [startDate, endDate] = toDates(origin, day, start, duration); return { id: `${id}-${index}`, start: toIso(startDate), end: toIso(endDate), locations: [location], assigned: idToAssigned.get(`${id}-${index}`), interested: counts.get(`${id}-${index}`), }; } function toShift(origin: Date, id: string, shorthand: string, index: number, idToAssigned: Map): ShiftSlot { const [day, start, duration] = shorthand.split(" "); const [startDate, endDate] = toDates(origin, day, start, duration); return { id: `${id}-${index}`, start: toIso(startDate), end: toIso(endDate), assigned: idToAssigned.get(`${id}-${index}`), }; } export function generateDemoSchedule(): Schedule { const origin = new Date(); const utcOffset = 1; origin.setUTCDate(origin.getUTCDate() - origin.getUTCDay() + 1); // Go to Monday origin.setUTCHours(-utcOffset); origin.setUTCMinutes(0); origin.setUTCSeconds(0); origin.setUTCMilliseconds(0); const counts = new Map() const accounts = generateDemoAccounts(); for (const account of accounts) { for (const id of account.interestedIds ?? []) { counts.set(id, (counts.get(id) ?? 0) + 1); } } seed = 2; const idToAssigned = new Map(); for (const account of accounts.filter(a => a.type === "crew" || a.type === "admin")) { const assignedIds: string[] = []; const slotsToAdd = Math.floor(random() * 20); while (assignedIds.length < slotsToAdd) { const event = events[Math.floor(random() * events.length)]; const eventId = toId(event.name); if (assignedIds.some(id => id.replace(/-\d+$/, "") === eventId)) { continue; } if (event.slots.length === 1 || random() < 0.8) { for (const index of event.slots.map((_, index) => index)) { assignedIds.push(toId(`${toId(event.name)}-${index}`)); } } else { for (const index of event.slots.map((_, index) => index)) { if (random() < 0.5) { assignedIds.push(toId(`${toId(event.name)}-${index}`)); } } } } for (const id of assignedIds) { const assigned = idToAssigned.get(id); if (assigned) { assigned.push(account.id); } else { idToAssigned.set(id, [account.id]); } } } seed = 5; for (const account of accounts.filter(a => a.type === "crew" || a.type === "admin")) { const assignedIds: string[] = []; const slotsToAdd = Math.floor(random() * 3); while (assignedIds.length < slotsToAdd) { const shift = rota[Math.floor(random() * rota.length)]; const shiftId = toId(shift.name); if (assignedIds.some(id => id.replace(/-\d+$/, "") === shiftId)) { continue; } if (shift.slots.length === 1 || random() < 0.8) { for (const index of shift.slots.map((_, index) => index)) { assignedIds.push(toId(`${toId(shift.name)}-${index}`)); } } else { for (const index of shift.slots.map((_, index) => index)) { if (random() < 0.5) { assignedIds.push(toId(`${toId(shift.name)}-${index}`)); } } } } for (const id of assignedIds) { const assigned = idToAssigned.get(id); if (assigned) { assigned.push(account.id); } else { idToAssigned.set(id, [account.id]); } } } return { events: events.map( ({ name, crew, description, slots }) => ({ id: toId(name), name, crew, description, interested: counts.get(toId(name)), slots: slots.map((shorthand, index) => toSlot(origin, toId(name), shorthand, index, counts, idToAssigned)) }) ), locations: locations.map( ({ name, description }) => ({ id: toId(name), name, description }) ), roles, rota: rota.map( ({ name, role, slots }) => ({ id: toId(name), name, role, slots: slots.map((shorthand, index) => toShift(origin, toId(name), shorthand, index, idToAssigned)) }) ) }; } const names = [ "Leo", "Lisa", "Jack", "Emily", "Roy", "Sofia", "Adam", "Eve", "Max", "Rose", "Hugo", "Maria", "David", "Zoe", "Hunter", "Ria", "Sonny", "Amy", "Kai", "Megan", "Toby", "Katie", "Bob", "Lucy", ]; // MINSTD random implementation for reproducible random numbers. let seed = 1; function random() { const a = 48271; const c = 0; const m = 2 ** 31 -1; return (seed = (a * seed + c) % m | 0) / 2 ** 31; } export function generateDemoAccounts(): Account[] { seed = 1; const accounts: Account[] = []; for (const name of names) { accounts.push({ id: accounts.length, name, type: (["regular", "crew", "crew", "crew", "admin"] as const)[Math.floor(random() ** 3 * 5)], }); } seed = 1; // These have a much higher probability of being in someone's interested list. const desiredEvent = ["opening", "closing", "fursuit-games"]; const nonCrewEvents = events.filter(event => !event.crew); for (const account of accounts) { const interestedIds: string[] = []; for (const id of desiredEvent) { if (random() < 0.5) { interestedIds.push(id); } } const eventsToAdd = Math.floor(random() * 10); while (interestedIds.length < eventsToAdd) { const event = nonCrewEvents[Math.floor(random() * nonCrewEvents.length)]; const eventId = toId(event.name); if (interestedIds.some(id => id.replace(/-\d+$/, "") === eventId)) { continue; } if (event.slots.length === 1 || random() < 0.8) { interestedIds.push(toId(event.name)) } else { for (const index of event.slots.map((_, index) => index)) { if (random() < 0.5) { interestedIds.push(toId(`${toId(event.name)}-${index}`)); } } } } if (interestedIds.length) { account.interestedIds = interestedIds; } } return accounts; }