owltide/server/generate-demo-schedule.ts

416 lines
11 KiB
TypeScript

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<string, number>, idToAssigned: Map<string, number[]>): 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<string, number[]>): 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<string, number>()
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<string, number[]>();
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;
}