[ADD] prng: PCG32 checked against the reference C implementation

The demonstrations and the search must give the same plan from the same
seed on any machine, so every draw comes from one generator with explicit
state. PCG32 runs on 16-bit limbs without BigInt, seeded as
pcg32_srandom_r; bounded draws reject as pcg32_boundedrand_r does, and the
shuffle loop is the reference demo's. Expected values come from compiling
and running the reference implementation, never from this code.

Checked: outputs, coins, dice and deck match the reference demo; a wrong
multiplier, rotation or remainder each fail.

--- FR ---

[ADD] prng : PCG32 vérifié contre l'implémentation C de référence

Les démonstrations et la recherche doivent rendre le même plan pour la
même graine sur toute machine : chaque tirage vient d'un seul générateur à
état explicite. PCG32 calcule par mots de 16 bits sans BigInt, ensemencé
comme pcg32_srandom_r ; le tirage borné rejette comme
pcg32_boundedrand_r, et la boucle de mélange est celle de la démo de
référence. Les valeurs attendues sortent de l'implémentation de référence
compilée et exécutée, jamais de ce code.

Vérifié : sorties, pièces, dés et paquet égalent la démo de référence ;
un multiplicateur, une rotation ou un reste faussés échouent chacun.

Assisted-by: Claude Opus 5.5
This commit is contained in:
Mathieu Benoit 2026-10-05 17:27:06 -04:00
parent 100c0d3399
commit cd9ea442a6
3 changed files with 488 additions and 0 deletions

View file

@ -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é.'

143
src/demo/prng.js Normal file
View file

@ -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 };
}

283
src/demo/prng.test.js Normal file
View file

@ -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);
});
});