diff --git a/scripts/oracle/pcg32_reference.sh b/scripts/oracle/pcg32_reference.sh new file mode 100755 index 0000000..35fc636 --- /dev/null +++ b/scripts/oracle/pcg32_reference.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# © 2026 TechnoLibre (http://www.technolibre.ca) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +# Imprime la sortie de la démo de l'implémentation de référence de PCG32, que +# rejoue src/demo/prng.test.js (§ 14.11 : un vecteur pris hors du code éprouvé). +# +# scripts/oracle/pcg32_reference.sh +# +# Le script récupère pcg-c-basic en profondeur 1 dans un répertoire +# temporaire, au commit que cite src/demo/prng.test.js, et imprime le hachage +# du commit extrait. Il compile pcg32-demo.c avec pcg_basic.c (gcc -O2), puis +# exécute la démo sans argument : elle ensemence par +# pcg32_srandom_r(&rng, 42u, 54u) et imprime cinq tours de tirages. Le commit +# est demandé par son hachage, et non comme tête d'une branche : le dépôt peut +# avancer sans changer ce que le script compile. Le répertoire temporaire est +# retiré à la sortie. La récupération demande un accès réseau, et échoue +# plutôt que de demander un identifiant. +# +# Sortie : 0, la démo a tourné ; 1, outil absent, récupération, compilation ou +# exécution en échec ; 2, argument reçu. +set -euo pipefail + +readonly DEPOT='https://github.com/imneme/pcg-c-basic' +readonly COMMIT='bc39cd76ac3d541e618606bcc6e1e5ba5e5e6aa3' + +refuser() { + printf 'Oracle PCG32 : %s\n' "$1" >&2 + exit 1 +} + +if (($# > 0)); then + printf 'usage : scripts/oracle/pcg32_reference.sh\n' >&2 + exit 2 +fi + +for outil in git gcc; do + command -v "$outil" >/dev/null || refuser "$outil introuvable." +done + +temporaire="$(mktemp -d)" +trap 'rm -rf -- "$temporaire"' EXIT +source_c="$temporaire/pcg-c-basic" +demo="$temporaire/pcg32-demo" + +# Un dépôt vide reçoit le seul commit COMMIT, sans historique, et l'extrait. +recuperer() { + git init --quiet -- "$source_c" && + GIT_TERMINAL_PROMPT=0 git -C "$source_c" fetch --quiet --depth 1 -- "$DEPOT" "$COMMIT" && + git -C "$source_c" checkout --quiet FETCH_HEAD +} +recuperer || refuser "récupération de $DEPOT au commit $COMMIT impossible." +commit="$(git -C "$source_c" rev-parse HEAD)" + +gcc -O2 -o "$demo" "$source_c/pcg32-demo.c" "$source_c/pcg_basic.c" \ + || refuser 'compilation de pcg32-demo.c et pcg_basic.c impossible.' + +printf 'dépôt : %s\n' "$DEPOT" +printf 'commit : %s\n' "$commit" +printf 'compilation : gcc -O2 pcg32-demo.c pcg_basic.c\n' +printf 'exécution : pcg32-demo, sans argument\n\n' +"$demo" || refuser 'la démo a échoué.' diff --git a/src/demo/prng.js b/src/demo/prng.js new file mode 100644 index 0000000..eedcf60 --- /dev/null +++ b/src/demo/prng.js @@ -0,0 +1,143 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Le générateur pseudo-aléatoire unique du projet (§ 15.5, point 1) : PCG32, +// variante XSH RR 64/32 de pcg_basic.c. L'état et l'incrément sont des entiers +// de 64 bits, portés chacun en deux mots non signés de 32 bits, haut et bas. +// Un pas fait état ← état × 6364136223846793005 + incrément, modulo 2^64 ; +// chaque tirage rend 32 bits de l'état d'avant le pas, mélangés par la +// fonction de sortie XSH RR. Tout le calcul tient en entiers exacts sur des +// nombres flottants, sans BigInt ni lecture de l'environnement : même graine, +// même flux, même suite, sur tout moteur d'exécution. L'ordre des tirages +// fait partie du contrat (§ 15.5, point 3) : suivant consomme un tirage ; +// borne, un par essai, rejets compris ; melanger, ceux de ses n − 1 appels à +// borne. + +// 6364136223846793005, le multiplicateur de pcg_basic.c, en deux mots. +const MULTIPLICATEUR_HAUT = 0x5851f42d; +const MULTIPLICATEUR_BAS = 0x4c957f2d; +const DEUX_32 = 0x100000000; +const MOT_MAX = 0xffffffff; + +/** + * Flux (initseq de pcg32_srandom_r) réservés, un par usage : à graine égale, + * deux usages tirent deux suites différentes. + */ +export const FLUX = Object.freeze({ GRAINES: 1, RECHERCHE: 2, DEMO: 3 }); + +// Produit des 64 bits bas de (ahi:alo) × (bhi:blo). Chaque colonne reste sous +// 2^35, donc exacte en double ; la retenue se propage par division entière. +function mul64(ahi, alo, bhi, blo) { + const a0 = alo & 0xffff, a1 = alo >>> 16, a2 = ahi & 0xffff, a3 = ahi >>> 16; + const b0 = blo & 0xffff, b1 = blo >>> 16, b2 = bhi & 0xffff, b3 = bhi >>> 16; + let t = a0 * b0; + const r0 = t % 65536; t = Math.floor(t / 65536) + a0 * b1 + a1 * b0; + const r1 = t % 65536; t = Math.floor(t / 65536) + a0 * b2 + a1 * b1 + a2 * b0; + const r2 = t % 65536; t = Math.floor(t / 65536) + a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0; + const r3 = t % 65536; + return [(r3 * 65536 + r2) >>> 0, (r1 * 65536 + r0) >>> 0]; +} + +// Écrit une valeur reçue dans un message d'erreur : un nombre tel quel, toute +// autre valeur suivie de son type. +function decrire(valeur) { + return typeof valeur === 'number' ? String(valeur) : `${String(valeur)} (${typeof valeur})`; +} + +// Lève une RangeError nommant le paramètre quand la valeur n'est pas un +// entier de min à max. +function exigerEntier(nom, valeur, min, max) { + if (!Number.isInteger(valeur) || valeur < min || valeur > max) { + throw new RangeError(`${nom} : entier de ${min} à ${max} attendu, reçu ${decrire(valeur)}`); + } +} + +/** + * Générateur PCG32 (XSH RR 64/32), état 64 bits porté en deux mots de 32 bits. + * Ensemencement identique à pcg32_srandom_r(initstate, initseq). + * + * L'objet rendu porte : + * - suivant() : entier non signé de 32 bits, pcg32_random_r ; + * - borne(n) : entier uniforme de [0, n), n entier de 1 à 2^32 − 1, par le + * rejet de pcg32_boundedrand_r ; + * - melanger(tab) : mélange tab en place et le rend, par la boucle de + * pcg32-demo.c — pour i de n à 2, échange l'élément borne(i) et + * l'élément i − 1. + * + * @param {number} graine initstate, entier de 0 à 2^32 − 1 + * @param {number} flux initseq, entier de 0 à 2^32 − 1 (voir FLUX) + * @returns {{suivant: function(): number, borne: function(number): number, + * melanger: function(Array|Int32Array): (Array|Int32Array)}} + * @throws {RangeError} graine ou flux hors de cet intervalle ; borne(n) pour + * n hors du sien + */ +export function creerPcg32(graine, flux) { + exigerEntier('graine', graine, 0, MOT_MAX); + exigerEntier('flux', flux, 0, MOT_MAX); + + // Incrément impair (flux << 1) | 1 : le bit 31 du flux passe au mot haut. + const incrementHaut = flux >>> 31; + const incrementBas = ((flux << 1) | 1) >>> 0; + let haut = 0; + let bas = 0; + + // état ← état + (h:b), modulo 2^64. La somme des mots bas, sous 2^33, est + // exacte ; sa retenue passe au mot haut. + function ajouter(h, b) { + const somme = bas + b; + bas = somme >>> 0; + haut = (haut + h + (somme >= DEUX_32 ? 1 : 0)) >>> 0; + } + + function suivant() { + const ancienHaut = haut; + const ancienBas = bas; + [haut, bas] = mul64(haut, bas, MULTIPLICATEUR_HAUT, MULTIPLICATEUR_BAS); + ajouter(incrementHaut, incrementBas); + // Sortie XSH RR de l'état d'avant le pas : les bits 27 à 58 de + // état ^ (état >> 18), tournés à droite du nombre que forment les 5 bits + // hauts de l'état. + const xHaut = ancienHaut ^ (ancienHaut >>> 18); + const xBas = ancienBas ^ ((ancienBas >>> 18) | (ancienHaut << 14)); + const melange = (xBas >>> 27) | (xHaut << 5); + const rotation = ancienHaut >>> 27; + return ((melange >>> rotation) | (melange << (-rotation & 31))) >>> 0; + } + + // Les tirages sous le seuil (2^32 − n) mod n sont rejetés : les + // 2^32 − seuil valeurs restantes, multiple de n, se répartissent également + // entre les n restes. La garde précède le seuil, qui vaudrait NaN pour + // n = 0. La boucle tire tant que r < seuil : pour un seuil entier, elle + // accepte les mêmes tirages que le « r >= seuil » de pcg32_boundedrand_r ; + // pour un seuil NaN, elle rend après un tirage, là où cette forme ne + // rendrait jamais. Une garde affaiblie fait ainsi échouer l'épreuve des + // bornes invalides au lieu de la figer : une boucle synchrone sans fin + // échappe au délai du lanceur. + function borne(n) { + exigerEntier('borne', n, 1, MOT_MAX); + const seuil = (DEUX_32 - n) % n; + let r; + do { + r = suivant(); + } while (r < seuil); + return r % n; + } + + // Un tableau de moins de deux éléments ne consomme aucun tirage. + function melanger(tab) { + for (let i = tab.length; i > 1; i -= 1) { + const j = borne(i); + const element = tab[j]; + tab[j] = tab[i - 1]; + tab[i - 1] = element; + } + return tab; + } + + // pcg32_srandom_r : état nul, un pas, ajout de la graine, un pas. + suivant(); + ajouter(0, graine); + suivant(); + + return { suivant, borne, melanger }; +} diff --git a/src/demo/prng.test.js b/src/demo/prng.test.js new file mode 100644 index 0000000..714c9e3 --- /dev/null +++ b/src/demo/prng.test.js @@ -0,0 +1,283 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Le vecteur de PCG32 vient de l'implémentation de référence, hors de ce +// dépôt : c'est un oracle, non un témoin (§ 14.11). Dépôt +// https://github.com/imneme/pcg-c-basic, commit +// bc39cd76ac3d541e618606bcc6e1e5ba5e5e6aa3 : pcg32-demo.c, compilé avec +// pcg_basic.c et exécuté sans argument, ensemence par +// pcg32_srandom_r(&rng, 42u, 54u) et imprime cinq tours de tirages. +// scripts/oracle/pcg32_reference.sh refait ces gestes ; TOUR_1 recopie le +// premier tour de sa sortie. +import assert from 'node:assert/strict'; +import { performance } from 'node:perf_hooks'; +import { describe, test } from '../../test/lanceur.js'; +import { FLUX, creerPcg32 } from './prng.js'; + +const DEUX_32 = 2 ** 32; + +// Premier tour de la démo, ligne par ligne, tel qu'elle l'imprime. Ses tirages +// se suivent dans cet ordre : six sorties de pcg32_random_r ; 65 pièces, +// pcg32_boundedrand_r(&rng, 2), imprimées H pour 1 et T pour 0 ; 33 dés, +// pcg32_boundedrand_r(&rng, 6) + 1 ; puis le mélange d'un paquet de 52 cartes +// numérotées de 0 à 51. La carte c s'imprime par sa valeur, de rang c / 4 en +// division entière dans « A23456789TJQK », puis sa couleur, de rang c % 4 dans +// « hcds ». La démo coupe le paquet en lignes de 22 cartes, jointes ici par +// une espace. +const TOUR_1 = { + sorties: '0xa15c02b7 0x7b47f409 0xba1d3330 0x83d2f293 0xbfa4784b 0xcbed606e', + pieces: 'HHTTTHTHHHTHTTTHHHHHTTTHHHTHTHTHTTHTTTHHHHHHTTTTHHTTTTTHTTTTTTTHT', + des: '3 4 1 1 2 2 3 2 4 3 2 4 3 3 5 2 3 1 3 1 5 1 4 1 5 6 4 6 6 2 6 3 3', + cartes: + 'Qd Ks 6d 3s 3d 4c 3h Td Kc 5c Jh Kd Jd As 4s 4h Ad Th Ac Jc 7s Qs ' + + '2s 7h Kh 2d 6c Ah 4d Qh 9h 6s 5s 2c 9c Ts 8d 9s 3c 8c Js 5d 2h 6h ' + + '7d 8s 9d 5h 8h Qc 7c Tc', +}; + +// Une sortie de 32 bits, écrite comme la démo l'imprime (« 0x%08x »). +const hexa = (x) => `0x${x.toString(16).padStart(8, '0')}`; + +// Une carte, écrite comme la démo l'imprime. +const carte = (c) => 'A23456789TJQK'[Math.floor(c / 4)] + 'hcds'[c % 4]; + +// Tire le premier tour de la démo, dans son ordre, et l'écrit comme elle. +function premierTour(rng) { + const sorties = Array.from({ length: 6 }, () => hexa(rng.suivant())).join(' '); + const pieces = Array.from(TOUR_1.pieces, () => (rng.borne(2) ? 'H' : 'T')).join(''); + const des = TOUR_1.des.split(' ').map(() => rng.borne(6) + 1).join(' '); + const paquet = Array.from({ length: 52 }, (_, c) => c); + rng.melanger(paquet); + return { sorties, pieces, des, cartes: paquet.map(carte).join(' ') }; +} + +// Transcription de pcg32_srandom_r et pcg32_random_r en BigInt, modulo 2^64 : +// elle ne partage rien de l'arithmétique par mots de 32 bits de prng.js. Rend +// la fonction de tirage, déjà ensemencée. +const MASQUE_64 = (1n << 64n) - 1n; +const MASQUE_32 = (1n << 32n) - 1n; +function transcriptionBigInt(graine, flux) { + const increment = ((BigInt(flux) << 1n) | 1n) & MASQUE_64; + let etat = 0n; + function tirer() { + const ancien = etat; + etat = (ancien * 6364136223846793005n + increment) & MASQUE_64; + const melange = (((ancien >> 18n) ^ ancien) >> 27n) & MASQUE_32; + const rotation = ancien >> 59n; + return Number(((melange >> rotation) | (melange << ((32n - rotation) & 31n))) & MASQUE_32); + } + tirer(); + etat = (etat + BigInt(graine)) & MASQUE_64; + tirer(); + return tirer; +} + +// Les valeurs que l'appel admet, ou refuse par une autre erreur qu'une +// RangeError. Recueillies puis comparées à [], elles sont toutes nommées en un +// seul échec. +function admisesATort(valeurs, appel) { + return valeurs.filter((valeur) => { + try { + appel(valeur); + return true; + } catch (erreur) { + return !(erreur instanceof RangeError); + } + }); +} + +describe('PCG32', () => { + test('rejoue le premier tour de la démo de référence (graine 42, flux 54)', () => { + assert.deepEqual(premierTour(creerPcg32(42, 54)), TOUR_1); + }); + + // Le vecteur n'exerce ni le mot haut de l'incrément, nul pour le flux 54, + // ni la retenue des additions sur 64 bits : celle de l'incrément 109 ne + // survient que pour un mot bas de produit d'au moins 2^32 − 109, une fois + // sur quarante millions de pas ; celle de la graine 42, ajoutée à l'état + // 109, jamais. Un flux d'au moins 2^31 porte le mot haut. Le flux 2^32 − 1 + // déclenche la retenue de l'incrément à presque chaque pas, la graine + // 2^32 − 1 sous le flux 0 celle de l'ensemencement seule, et les deux + // ensemble les déclenchent toutes deux. Sur ces cas, le test confronte + // prng.js à la transcription BigInt. C'est une contre-vérification de la + // décomposition en mots de 32 bits, non un oracle (§ 14.11) : la + // transcription ne s'ancre que sur les six sorties du vecteur, qui + // n'exercent pas ces chemins, et elle suit la même lecture de pcg_basic.c + // que prng.js. Une erreur de lecture commune aux deux, comme un incrément + // calculé sur 32 bits, y resterait verte. + test('concorde avec une transcription BigInt de pcg_basic.c aux graines et flux extrêmes', () => { + const ancre = transcriptionBigInt(42, 54); + assert.equal(Array.from({ length: 6 }, () => hexa(ancre())).join(' '), TOUR_1.sorties); + + const cas = [ + [0, 0], + [0xffffffff, 0], + [0, 0x80000000], + [0x80000000, 0x80000000], + [0, 0xffffffff], + [0xffffffff, 0xffffffff], + [42, FLUX.GRAINES], + [42, FLUX.RECHERCHE], + [42, FLUX.DEMO], + ]; + const ecarts = []; + for (const [graine, flux] of cas) { + const rng = creerPcg32(graine, flux); + const reference = transcriptionBigInt(graine, flux); + for (let tirage = 0; tirage < 1000; tirage += 1) { + const [obtenu, attendu] = [rng.suivant(), reference()]; + if (obtenu !== attendu) { + ecarts.push(`graine ${graine}, flux ${flux}, tirage ${tirage} : ${obtenu} au lieu de ${attendu}`); + break; + } + } + } + assert.deepEqual(ecarts, []); + }); + + // Chaque générateur porte son propre état et son propre incrément. Deux + // générateurs vivants à la fois, sur deux flux de FLUX, rendent chacun la + // suite qu'il rendrait seul. Les tirages alternent : un état ou un + // incrément rangé hors de l'instance, donc partagé, décale alors les deux + // suites. Les deux flux donnent deux suites distinctes, sans quoi un + // incrément partagé ne changerait aucun tirage. + test('deux générateurs de flux différents, tirés en alternance, gardent chacun sa suite', () => { + const seul = (flux) => { + const rng = creerPcg32(42, flux); + return Array.from({ length: 50 }, () => rng.suivant()); + }; + const attendu = [seul(FLUX.GRAINES), seul(FLUX.RECHERCHE)]; + assert.notDeepEqual(attendu[0], attendu[1]); + + const a = creerPcg32(42, FLUX.GRAINES); + const b = creerPcg32(42, FLUX.RECHERCHE); + const obtenu = [[], []]; + for (let i = 0; i < 50; i += 1) { + obtenu[0].push(a.suivant()); + obtenu[1].push(b.suivant()); + } + assert.deepEqual(obtenu, attendu); + }); + + // Les bornes du vecteur, 2, 6 puis 52 à 2, ont un seuil sous 2^6 : leur + // rejet ne survient pratiquement jamais. Le test prend une borne de chaque + // côté de 2^31. Pour n = 2^31 + 1, 2^32 − n est sous n et le modulo n'agit + // pas : le seuil vaut 2^31 − 1, et près d'un tirage sur deux est rejeté. + // Pour n = 2^30 + 1, il agit : le seuil vaut 2^32 − 3n = 2^30 − 3, et un + // tirage sur quatre environ est rejeté. Une négation sur 32 bits signés, + // ~n + 1, ne vaut 2^32 − n qu'au-dessus de 2^31 : le seuil qu'elle donne + // n'est faux que sous 2^31, et seule la seconde borne le voit. borne + // consomme un tirage par rejet et un par résultat : les résultats attendus + // sont les tirages acceptés d'un jumeau, réduits modulo n, dans l'ordre. + test('borne rejette les tirages sous le seuil (2^32 − n) mod n, comme pcg32_boundedrand_r', () => { + const jumeau = creerPcg32(42, 54); + const tirages = Array.from({ length: 200 }, () => jumeau.suivant()); + for (const [n, seuil] of [ + [2 ** 31 + 1, 2 ** 31 - 1], + [2 ** 30 + 1, 2 ** 30 - 3], + ]) { + assert.equal((DEUX_32 - n) % n, seuil); + const acceptes = tirages.filter((r) => r >= seuil); + assert.ok(acceptes.length > 0, `n = ${n} : aucun tirage accepté`); + assert.ok(acceptes.length < tirages.length, `n = ${n} : aucun tirage rejeté, le rejet reste inexercé`); + + const rng = creerPcg32(42, 54); + assert.deepEqual( + { n, resultats: acceptes.map(() => rng.borne(n)) }, + { n, resultats: acceptes.map((r) => r % n) }, + ); + } + }); + + // Le deuxième tirage du vecteur, 0x7b47f409, est exactement le seuil de + // n = 2^32 − 0x7b47f409 : n dépasse 2^31, donc (2^32 − n) mod n = 2^32 − n. + // Accepté, il revient tel quel, puisqu'il est sous n. + test('borne accepte un tirage égal au seuil', () => { + const rng = creerPcg32(42, 54); + rng.suivant(); + assert.equal(rng.borne(DEUX_32 - 0x7b47f409), 0x7b47f409); + }); + + // Pour n = 2^32 − 0x7b47f409 − 1, le seuil vaut de même 2^32 − n, soit + // 0x7b47f40a, un de plus que le deuxième tirage du vecteur : borne rejette + // celui-ci et rend le troisième, 0xba1d3330, réduit modulo n. Avec + // « borne accepte un tirage égal au seuil », ce test fixe le seuil des deux + // côtés : un seuil calculé sur 2^32 − 1 au lieu de 2^32 accepterait le + // deuxième tirage et décalerait tous les suivants. + test('borne rejette un tirage juste sous le seuil', () => { + const rng = creerPcg32(42, 54); + rng.suivant(); + const n = DEUX_32 - 0x7b47f409 - 1; + assert.equal(rng.borne(n), 0xba1d3330 % n); + }); + + test("borne(1) rend toujours 0, au prix d'un tirage comme pcg32_boundedrand_r", () => { + const rng = creerPcg32(42, 54); + const jumeau = creerPcg32(42, 54); + for (let i = 0; i < 1000; i += 1) { + assert.equal(rng.borne(1), 0); + jumeau.suivant(); + } + assert.equal(rng.suivant(), jumeau.suivant()); + }); + + // 2^32 − 1 est la plus grande borne de pcg32_boundedrand_r, dont le + // paramètre est un uint32_t. Son seuil vaut 1 : le premier tirage du + // vecteur, 0xa15c02b7, passe et revient tel quel, puisqu'il est sous n. + test('borne admet 2^32 − 1, la plus grande borne de pcg32_boundedrand_r', () => { + assert.equal(creerPcg32(42, 54).borne(DEUX_32 - 1), 0xa15c02b7); + }); + + test('borne refuse 0, et toute borne hors des entiers de 1 à 2^32 − 1', () => { + const rng = creerPcg32(42, 54); + const invalides = [0, -1, 1.5, DEUX_32, NaN, Infinity, '6', 6n, undefined]; + assert.deepEqual(admisesATort(invalides, (n) => rng.borne(n)), []); + }); + + // La boucle de la démo tire borne(i) pour i de n à 2 : un tableau de moins + // de deux éléments ne consomme aucun tirage. Un tirage de trop en fin de + // mélange échappe au vecteur, dont le paquet clôt le premier tour ; le + // jumeau, qui tire les mêmes bornes, le voit au tirage suivant. + test('melanger suit la boucle de la démo, en place : borne(i) pour i de n à 2', () => { + const rng = creerPcg32(42, 54); + const jumeau = creerPcg32(42, 54); + const tab = [10, 20, 30, 40, 50]; + assert.equal(rng.melanger(tab), tab); + for (let i = tab.length; i > 1; i -= 1) jumeau.borne(i); + rng.melanger([]); + rng.melanger([7]); + assert.equal(rng.suivant(), jumeau.suivant()); + assert.deepEqual([...tab].sort((a, b) => a - b), [10, 20, 30, 40, 50]); + }); + + // Une graine hors de l'intervalle, tronquée en silence, désignerait une + // autre graine ; un flux omis deviendrait le flux 0. + test('creerPcg32 refuse une graine ou un flux hors des entiers de 0 à 2^32 − 1', () => { + const invalides = [-1, 1.5, DEUX_32, NaN, Infinity, '42', 42n, undefined]; + assert.deepEqual(admisesATort(invalides, (graine) => creerPcg32(graine, 54)), []); + assert.deepEqual(admisesATort(invalides, (flux) => creerPcg32(42, flux)), []); + }); + + // Témoin, non oracle : changer un flux change chaque graine dérivée et + // chaque démonstration. Le test fait de ce changement un geste conscient. + test('FLUX attribue les flux 1, 2 et 3, figés', () => { + assert.deepEqual(FLUX, { GRAINES: 1, RECHERCHE: 2, DEMO: 3 }); + assert.ok(Object.isFrozen(FLUX)); + }); + + // La durée est affichée, jamais affirmée : la cible est sous une seconde, + // et une machine chargée la dépasse sans que le code soit en cause + // (§ 14.14). L'assertion porte sur les tirages, entiers non signés de 32 + // bits ; x >>> 0 diffère de x pour toute autre valeur. + test('1 000 000 tirages : durée affichée, cible sous une seconde', () => { + const rng = creerPcg32(42, FLUX.DEMO); + let horsMot = 0; + const debut = performance.now(); + for (let i = 0; i < 1_000_000; i += 1) { + const x = rng.suivant(); + if (x !== x >>> 0) horsMot += 1; + } + const duree = performance.now() - debut; + console.log(`PCG32 : 1 000 000 tirages en ${duree.toFixed(1)} ms (cible : moins de 1 000 ms)`); + assert.equal(horsMot, 0); + }); +});