Files
MineGeo/shared/tournaments.ts
pavel444-byte de0e891b62
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 3m40s
Add Void difficulty and publish changelogs
2026-10-20 14:05:01 +05:00

117 lines
3.5 KiB
TypeScript

import { z } from "zod";
import type { RoomView } from "./multiplayer";
const safeName = z
.string()
.trim()
.min(3)
.max(80)
.refine(
(value) =>
!/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/u.test(value),
"Names cannot contain hidden controls",
);
export const tournamentSettings = z
.object({
name: safeName,
datasetId: z.string().uuid(),
difficulty: z.union([
z.enum(["easy", "normal", "hard", "void"]),
z.string().uuid(),
]),
playerLimit: z.number().int().min(2).max(32),
roundCount: z.number().int().min(1).max(10),
roundTimeSeconds: z.number().int().min(10).max(3000),
timerMode: z.enum(["shared", "perMini"]).default("shared"),
startsAt: z.string().datetime({ offset: true }),
announceDiscord: z.boolean(),
})
.strict()
.refine(
(value) =>
value.difficulty === "void"
? value.timerMode === "perMini" && value.roundTimeSeconds === 10
: value.roundTimeSeconds >= 15 && value.roundTimeSeconds % 15 === 0,
"Void requires a separate 10-second timer; other tournament timers use 15-second steps",
)
.refine(
(value) => value.timerMode !== "perMini" || value.roundTimeSeconds <= 600,
"Mini-round timers allow at most 600 seconds",
);
export const tournamentEdit = z
.object({
startsAt: z.string().datetime({ offset: true }),
playerLimit: z.number().int().min(2).max(32),
announceDiscord: z.boolean(),
})
.strict();
export type TournamentRecord = {
id: string;
name: string;
owner_id: string | null;
starts_at: string;
server_timezone: string;
player_limit: number;
round_count: number;
round_time_seconds: number;
timer_mode: "shared" | "perMini";
status: "scheduled" | "running" | "finished" | "cancelled";
announce_discord: boolean;
finished_at: string | null;
winner_id: string | null;
};
export type TournamentView = {
id: string;
name: string;
ownerId: string | null;
datasetId: string;
startsAt: string;
serverTimezone: string;
playerLimit: number;
roundCount: number;
roundTimeSeconds: number;
timerMode: "shared" | "perMini";
roundDeadline?: string | null;
announceDiscord: boolean;
status: TournamentRecord["status"];
currentRound: number;
finishedAt: string | null;
winnerId: string | null;
you: string | null;
roomId: string | null;
currentRoom?: RoomView | null;
announcementState?:
| "pending"
| "sending"
| "sent"
| "skipped"
| "failed"
| null;
serverNow: number;
players: { accountId: string; name: string; total: number; rank: number }[];
};
/** Interpret a datetime-local value in the advertised server zone, never the browser zone. */
export function serverLocalToIso(value: string, zone: string) {
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(value))
throw new Error("Choose a valid start time");
const target = Date.parse(value + ":00Z");
if (!Number.isFinite(target)) throw new Error("Choose a valid start time");
const formatter = new Intl.DateTimeFormat("sv-SE", {
timeZone: zone,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hourCycle: "h23",
});
let instant = target;
for (let i = 0; i < 4; i++) {
const shown = formatter.format(new Date(instant)).replace(" ", "T");
if (shown === value) return new Date(instant).toISOString();
instant += target - Date.parse(shown + ":00Z");
}
throw new Error("That local time does not exist in the server timezone");
}