import rawLieux from './data/lieux.json'; import type { Categorie, Lieu } from '$lib/types'; const lieux = rawLieux as Lieu[]; /** Nombre de manches d'un défi. */ const NB_MANCHES = 5; /** Seed acceptée : alphanumérique + tiret/underscore, 4 à 40 caractères. */ const SEED_REGEX = /^[A-Za-z0-9_-]{4,40}$/; export function seedValide(seed: string): boolean { return SEED_REGEX.test(seed); } /** * Hash cyrb53 (domaine public) réduit à 32 bits. * Arithmétique entière via Math.imul : résultat strictement identique sur * toutes les plateformes — prérequis du déterminisme inter-machines (ADR-001 V1). */ function hashSeed(seed: string): number { let h1 = 0xdeadbeef; let h2 = 0x41c6ce57; for (let i = 0; i < seed.length; i++) { const ch = seed.charCodeAt(i); h1 = Math.imul(h1 ^ ch, 2654435761); h2 = Math.imul(h2 ^ ch, 1597334677); } h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909); h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909); return (h1 ^ h2) >>> 0; } /** PRNG mulberry32 (domaine public) : flottants dans [0, 1), déterministes. */ function mulberry32(graine: number): () => number { let a = graine; return () => { a |= 0; a = (a + 0x6d2b79f5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } /** * Tire les 5 lieux d'un défi à partir de la seed : mêmes lieux, même ordre, * pour tous les joueurs et sur toutes les machines (ADR-001 v1). * PRNG seedé exclusivement — jamais Math.random. * Contraintes : ≥ 1 MONUMENT et ≥ 1 LIEU, et si possible ≥ 2 régions distinctes. * Fonction pure : aucune I/O réseau, aucun état ; même entrée → même sortie. */ export function idsPourSeed(seed: string): string[] { const rng = mulberry32(hashSeed(seed)); // Mélange de Fisher-Yates déterministe des indices du corpus. const indices = lieux.map((_, i) => i); for (let i = indices.length - 1; i > 0; i--) { const j = Math.floor(rng() * (i + 1)); [indices[i], indices[j]] = [indices[j], indices[i]]; } const melanges = indices.map((i) => lieux[i]); const tires = melanges.slice(0, NB_MANCHES); // ≥ 1 de chaque catégorie : si l'une manque, on échange un tiré contre le // premier non-tiré de la catégorie manquante (l'autre catégorie reste présente). const assurerCategorie = (categorie: Categorie): void => { if (tires.some((l) => l.categorie === categorie)) return; const candidat = melanges.find((l) => l.categorie === categorie && !tires.includes(l)); if (candidat) tires[0] = candidat; }; assurerCategorie('MONUMENT'); assurerCategorie('LIEU'); // ≥ 2 régions distinctes si possible : on échange un tiré contre un non-tiré // d'une autre région et de MÊME catégorie (contrainte catégories préservée, // les deux étant présentes après l'étape précédente). const regions = new Set(tires.map((l) => l.region)); if (regions.size < 2) { const candidat = melanges.find((l) => !tires.includes(l) && !regions.has(l.region)); const i = candidat ? tires.findIndex((l) => l.categorie === candidat.categorie) : -1; if (candidat && i >= 0) tires[i] = candidat; } return tires.map((l) => l.id); }