import assert from "node:assert/strict"; import { test } from "node:test"; import { customDifficultySchema, difficulties, scoreGuess, } from "../shared/protocol"; import { createRoomSchema } from "../shared/multiplayer"; import { tournamentSettings } from "../shared/tournaments"; import { mergeDifficultyPresets } from "../shared/difficulty-presets"; const rules = { seconds: 90, zoom: true }; test("expanded difficulty rules preserve defaults and validate controls", () => { const preset = customDifficultySchema.parse({ name: "Classic", ...rules }); assert.deepEqual( { pan: preset.pan, compass: preset.compass, coordinates: preset.coordinates, mapZoom: preset.mapZoom, mapPan: preset.mapPan, timerVisible: preset.timerVisible, scoring: preset.scoring, }, { pan: true, compass: true, coordinates: true, mapZoom: true, mapPan: true, timerVisible: true, scoring: "normal", }, ); for (const key of [ "pan", "compass", "coordinates", "mapZoom", "mapPan", "timerVisible", ]) assert.equal( customDifficultySchema.safeParse({ name: "Fixture", ...rules, [key]: "false", }).success, false, ); assert.equal( customDifficultySchema.safeParse({ name: "Fixture", ...rules, scoring: "extreme", }).success, false, ); assert.equal( customDifficultySchema.safeParse({ name: "Fixture", ...rules, rounds: 10 }) .success, false, ); }); test("scoring strictness changes distance decay while perfect guesses remain 5000", () => { const bounds = { minX: 0, minZ: 0, maxX: 100, maxZ: 100 }; const actual = { x: 10, z: 10 }, guess = { x: 40, z: 40 }; const scores = ["strict", "normal", "gentle"].map( (mode) => scoreGuess(actual, guess, bounds, mode as "strict" | "normal" | "gentle") .score, ); assert.ok(scores[0] < scores[1] && scores[1] < scores[2]); for (const mode of ["strict", "normal", "gentle"] as const) assert.equal(scoreGuess(actual, actual, bounds, mode).score, 5000); }); test("difficulty names accept normal text and count Unicode characters consistently with PostgreSQL", () => { for (const name of [ "Normal", "Speed 90", "Тест", "中文", "😎😎", "😎".repeat(40), "a".repeat(40), "a\\b", ]) assert.equal( customDifficultySchema.safeParse({ name, ...rules }).success, true, name, ); assert.equal( customDifficultySchema.parse({ name: " My challenge ", ...rules }).name, "My challenge", ); for (const name of [ "", "a", "😎", "a".repeat(41), "😎".repeat(41), "x\nname", "x\u0085name", "x\u202ename", "x\u2066name", ]) assert.equal( customDifficultySchema.safeParse({ name, ...rules }).success, false, ); }); test("difficulty rules enforce their supported range and strict request shape", () => { for (const seconds of [5, 10, 15, 20, 90, 600]) assert.equal( customDifficultySchema.safeParse({ name: "Fixture", ...rules, seconds }) .success, true, ); for (const seconds of [0, 4, 14, 16, 601, 15.5, NaN, Infinity, "90"]) assert.equal( customDifficultySchema.safeParse({ name: "Fixture", ...rules, seconds }) .success, false, ); assert.equal( customDifficultySchema.safeParse({ name: "Fixture", ...rules, owner: "forged", }).success, false, ); }); test("Void is the shortest built-in challenge and disables navigation aids", () => { assert.ok( Object.entries(difficulties).every( ([id, rule]) => id === "void" || difficulties.void.seconds < rule.seconds, ), ); for (const rule of [ "zoom", "pan", "compass", "coordinates", "mapZoom", "mapPan", ] as const) assert.equal(difficulties.void[rule], false); assert.equal(difficulties.void.scoring, "strict"); assert.equal( createRoomSchema.safeParse({ datasetId: "11111111-1111-4111-8111-111111111111", difficulty: "void", playerLimit: 4, }).success, true, ); }); test("Void tournaments preserve ten seconds per mini-round", () => { const settings = { name: "Void event", datasetId: "11111111-1111-4111-8111-111111111111", difficulty: "void", playerLimit: 4, roundCount: 3, roundTimeSeconds: 10, timerMode: "perMini", startsAt: "2026-10-25T12:00:00Z", announceDiscord: false, }; assert.equal(tournamentSettings.safeParse(settings).success, true); assert.equal( tournamentSettings.safeParse({ ...settings, roundTimeSeconds: 90 }).success, false, ); assert.equal( tournamentSettings.safeParse({ ...settings, timerMode: "shared" }).success, false, ); assert.equal( tournamentSettings.safeParse({ ...settings, difficulty: "normal" }).success, false, ); }); test("a delayed preset listing cannot erase a newly saved preset or duplicate it", async () => { const old = { id: "old", name: "Old", ...rules }; const saved = { id: "saved", name: "New", ...rules }; let release!: (presets: (typeof old)[]) => void; const response = new Promise<(typeof old)[]>((resolve) => { release = resolve; }); let presets = [old]; const loading = response.then((snapshot) => { presets = mergeDifficultyPresets(presets, snapshot); }); presets = mergeDifficultyPresets(presets, [saved]); release([old]); await loading; assert.deepEqual( presets.map((p) => p.id), ["saved", "old"], ); assert.equal(mergeDifficultyPresets(presets, [saved]).length, 2); });