87 lines
3.2 KiB
TypeScript
87 lines
3.2 KiB
TypeScript
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);
|
|
}
|