Add Void difficulty and publish changelogs
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 3m40s

This commit is contained in:
2026-10-20 14:05:01 +05:00
parent 3ba398a838
commit de0e891b62
37 changed files with 995 additions and 65 deletions

51
shared/changelog.ts Normal file
View File

@@ -0,0 +1,51 @@
export const RELEASE_NEWS_CHANNEL = "1558048830770122772";
// A webhook ID is public; its secret URL/token must never be stored here.
export const RELEASE_WEBHOOK_ID = "1561021045211660401";
export type ReleasePost = {
id: string;
channel_id: string;
webhook_id?: string;
content: string;
timestamp: string;
};
export type ChangelogEntry = {
id: string;
title: string;
version: string;
kind: "Alpha" | "Beta" | "Production" | "Bug fix";
body: string;
publishedAt: string;
};
export function parseReleasePost(
post: ReleasePost,
webhookId = RELEASE_WEBHOOK_ID,
): ChangelogEntry | null {
if (post.webhook_id !== webhookId || !/^\d{17,20}$/.test(post.id))
return null;
const clean = post.content
.replace(/<@&\d+>/g, "")
.replace(/(?:^|\n)\s*PLAY THE MINEGEO AT\s*:[\s\S]*$/i, "")
.replace(/^[\s,.]+/, "")
.trim();
const header =
/^#{1,3}[ \t]+MineGeo[ \t]+(\d+\.\d+\.\d+(?:\.\d+)?)[ \t]+(Alpha|Beta|Production|Bug[ -]fix)([ \t]+[—–:-][^\r\n]*)?[ \t]*\r?\n/i.exec(
clean,
);
if (!header || !Number.isFinite(Date.parse(post.timestamp))) return null;
const body = clean.slice(header[0].length).trim();
if (!body) return null;
const kind = /^bug/i.test(header[2])
? "Bug fix"
: ((header[2][0].toUpperCase() +
header[2].slice(1).toLowerCase()) as ChangelogEntry["kind"]);
return {
id: post.id,
title: `MineGeo ${header[1]} ${kind}${header[3]?.trimEnd() ?? ""}`,
version: header[1],
kind,
body,
publishedAt: post.timestamp,
};
}

View File

@@ -238,6 +238,8 @@ export type Database = {
compass: boolean;
coordinates: boolean;
map_zoom: boolean;
map_pan: boolean;
timer_visible: boolean;
scoring: "normal" | "strict" | "gentle";
created_at: string;
id: string;
@@ -250,6 +252,8 @@ export type Database = {
compass?: boolean;
coordinates?: boolean;
map_zoom?: boolean;
map_pan?: boolean;
timer_visible?: boolean;
scoring?: "normal" | "strict" | "gentle";
created_at?: string;
id?: string;
@@ -262,6 +266,8 @@ export type Database = {
compass?: boolean;
coordinates?: boolean;
map_zoom?: boolean;
map_pan?: boolean;
timer_visible?: boolean;
scoring?: "normal" | "strict" | "gentle";
name?: string;
seconds?: number;

View File

@@ -190,7 +190,10 @@ export const developerOpenApi = {
type: "object",
properties: {
id,
difficulty: { type: "string", enum: ["easy", "normal", "hard"] },
difficulty: {
type: "string",
enum: ["easy", "normal", "hard", "void"],
},
createdAt: date,
currentRound: { type: "integer", minimum: 0, maximum: 5 },
complete: { type: "boolean" },

View File

@@ -3,6 +3,7 @@ export const leaderboardDifficulties = [
"easy",
"normal",
"hard",
"void",
] as const;
export type LeaderboardDifficulty = (typeof leaderboardDifficulties)[number];
export type LeaderboardEntry =

View File

@@ -1046,5 +1046,24 @@
"Minecraft map. Click to place a guess, or use arrow keys to pan and Enter to pin the center.": "Карта Minecraft. Нажмите для метки или перемещайтесь стрелками и нажмите Enter для метки в центре.",
"Strict scoring penalizes distance more; gentle scoring gives more credit for nearby guesses. Every game still has five rounds.": "Строгий подсчёт сильнее штрафует за расстояние, мягкий даёт больше очков за близкие метки. В игре по-прежнему пять раундов.",
"Guess selected": "Метка выбрана",
"Map": "Карта"
"Map": "Карта",
"Void": "Пустота",
"Hardest · No compass · Fixed map": "Самая сложная · Без компаса · Фиксированная карта",
"Allow guess-map panning": "Разрешить перемещение карты",
"Show round countdown": "Показывать обратный отсчёт",
"Minecraft map. Click to place a guess, or press Enter to pin the center.": "Карта Minecraft. Нажмите для метки или нажмите Enter, чтобы поставить метку в центре.",
"Easy gives you 180 seconds, Normal 90, and Hard 30. Void is the hardest: just 10 seconds, a fixed panorama and map, no zoom, no compass or coordinates, and strict scoring. A timed-out round scores zero; you can still finish the expedition.": "Лёгкая сложность даёт 180 секунд, обычная — 90, высокая — 30. Пустота — самая сложная: 10 секунд, фиксированные панорама и карта, без масштабирования, компаса и координат, со строгим подсчётом очков. За истёкший раунд начисляется ноль очков; экспедицию можно продолжить.",
"Meet MineGeo’s top explorers. Compare the best five-round scores across Easy, Normal, Hard, and Void Minecraft expeditions.": "Лучшие исследователи MineGeo. Сравнивайте результаты пяти раундов на лёгкой, обычной, высокой сложности и в Пустоте.",
"Changelogs": "История обновлений",
"WHAT'S NEW": "ЧТО НОВОГО",
"New releases, improvements, and fixes from MineGeo.": "Новые версии, улучшения и исправления MineGeo.",
"Latest releases": "Последние обновления",
"Release notes are temporarily unavailable": "История обновлений временно недоступна",
"Please check back soon.": "Пожалуйста, загляните позже.",
"No releases on this page yet.": "На этой странице пока нет обновлений.",
"Older posts": "Более ранние записи",
"Alpha": "Альфа",
"Beta": "Бета",
"Production": "Релиз",
"Bug fix": "Исправления ошибок"
}

View File

@@ -6,7 +6,7 @@ export const createRoomSchema = z
.object({
datasetId: z.string().uuid(),
difficulty: z.union([
z.enum(["easy", "normal", "hard"]),
z.enum(["easy", "normal", "hard", "void"]),
z.string().uuid(),
]),
playerLimit: z.number().int().min(2).max(16),

View File

@@ -65,7 +65,7 @@ export const metricDefinitions = {
minegeo_games: {
help: "Retained solo games by difficulty and completion state.",
labels: {
difficulty: ["easy", "normal", "hard"],
difficulty: ["easy", "normal", "hard", "void"],
status: ["incomplete", "complete"],
},
},

View File

@@ -183,6 +183,18 @@ export const difficulties = {
easy: { seconds: 180, zoom: true, label: "Easy" },
normal: { seconds: 90, zoom: true, label: "Normal" },
hard: { seconds: 30, zoom: false, label: "Hard" },
void: {
seconds: 10,
zoom: false,
label: "Void",
pan: false,
compass: false,
coordinates: false,
mapZoom: false,
mapPan: false,
timerVisible: true,
scoring: "strict",
},
} as const;
export type Difficulty = keyof typeof difficulties;
export const difficultyRuleDefaults = {
@@ -190,6 +202,8 @@ export const difficultyRuleDefaults = {
compass: true,
coordinates: true,
mapZoom: true,
mapPan: true,
timerVisible: true,
scoring: "normal" as const,
};
export type ScoringMode = "normal" | "strict" | "gentle";
@@ -203,6 +217,8 @@ export type DifficultyPreset = {
compass?: boolean;
coordinates?: boolean;
mapZoom?: boolean;
mapPan?: boolean;
timerVisible?: boolean;
scoring?: ScoringMode;
};
export const customDifficultySchema = z
@@ -222,14 +238,16 @@ export const customDifficultySchema = z
seconds: z
.number()
.int()
.min(15)
.min(5)
.max(600)
.refine((seconds) => seconds % 15 === 0),
.refine((seconds) => seconds % 5 === 0),
zoom: z.boolean(),
pan: z.boolean().default(true),
compass: z.boolean().default(true),
coordinates: z.boolean().default(true),
mapZoom: z.boolean().default(true),
mapPan: z.boolean().default(true),
timerVisible: z.boolean().default(true),
scoring: z.enum(["normal", "strict", "gentle"]).default("normal"),
})
.strict();

View File

@@ -16,17 +16,24 @@ export const tournamentSettings = z
name: safeName,
datasetId: z.string().uuid(),
difficulty: z.union([
z.enum(["easy", "normal", "hard"]),
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(15).max(3000).multipleOf(15),
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",