import { z } from "zod"; export const boundsSchema = z .object({ minX: z.number().int(), minZ: z.number().int(), maxX: z.number().int(), maxZ: z.number().int(), }) .refine( (b) => b.maxX > b.minX && b.maxZ > b.minZ && b.maxX - b.minX <= 2048 && b.maxZ - b.minZ <= 2048, "Bounds must be positive and at most 2048 blocks per side", ); export const pointSchema = z.object({ x: z.number().finite(), y: z.number().finite(), z: z.number().finite(), }); export const faces = ["px", "nx", "py", "ny", "pz", "nz"] as const; export const datasetSchema = z .object({ format: z.literal("minegeo-dataset/v1"), minecraftVersion: z.literal("1.21.11"), dimension: z.literal("minecraft:overworld"), source: z.enum(["plugin", "fabric"]), name: z.string().min(3).max(80), description: z.string().max(2000).default(""), serverName: z.string().min(2).max(80), serverAddress: z.string().max(255).optional(), bounds: boundsSchema, north: z.literal("-z"), viewpoints: z .array( pointSchema.extend({ id: z.string().regex(/^[a-zA-Z0-9_-]{1,64}$/) }), ) .min(5) .max(100), assets: z .array( z.object({ path: z .string() .regex(/^[a-zA-Z0-9_./-]+$/) .refine((s) => !s.includes("..") && !s.startsWith("/")), sha256: z.string().regex(/^[a-f0-9]{64}$/), bytes: z .number() .int() .positive() .max(50 * 1024 * 1024), }), ) .max(4096), }) .superRefine((d, ctx) => { if (new Set(d.viewpoints.map((p) => p.id)).size !== d.viewpoints.length) ctx.addIssue({ code: "custom", message: "Viewpoint IDs must be unique" }); if (new Set(d.assets.map((a) => a.path)).size !== d.assets.length) ctx.addIssue({ code: "custom", message: "Asset paths must be unique" }); for (const p of d.viewpoints) if ( p.x < d.bounds.minX || p.x > d.bounds.maxX || p.z < d.bounds.minZ || p.z > d.bounds.maxZ ) ctx.addIssue({ code: "custom", message: "Viewpoint outside map bounds", }); }); export type DatasetManifest = z.infer; export type Bounds = z.infer; export type Point = z.infer; export const difficulties = { easy: { seconds: 180, zoom: true, label: "Easy" }, normal: { seconds: 90, zoom: true, label: "Normal" }, hard: { seconds: 30, zoom: false, label: "Hard" }, } as const; export type Difficulty = keyof typeof difficulties; export function scoreGuess( a: Pick, b: Pick, bounds: Bounds, ) { const distance = Math.hypot(a.x - b.x, a.z - b.z); return { distance, score: distance <= 1 ? 5000 : Math.round( 5000 * Math.exp( (-5 * distance) / Math.hypot( bounds.maxX - bounds.minX, bounds.maxZ - bounds.minZ, ), ), ), }; } export type TerrainBlock = [number, number, number, string, string?]; export type TerrainSnapshot = { bounds: Bounds; blocks: TerrainBlock[]; coverage: [number, number][]; };