HOWTO · JavaScript
Use una semilla para generar números aleatorios en JavaScript
Este artículo aborda cómo usar una semilla para generar un número aleatorio en JavaScript. Logramos esto gracias a los PRNG que toman una semilla y devuelven un número aleatorio basado en esa semilla.
En esta página
Para generar números seudoaleatorios repetibles en JavaScript, inicializa un generador con una semilla y mantén iguales la semilla, la implementación del generador y el orden de las llamadas en cada ejecución. Math.random() no acepta una semilla, así que usa un PRNG con semilla, como los ejemplos siguientes, o seedrandom.js.
Cómo funciona la aleatoriedad con semilla
Math.random() de JavaScript no acepta una semilla. Para reproducir una secuencia, crea un generador de números seudoaleatorios (PRNG) que acepte un estado inicial. Si llamas a ese generador con la misma semilla, la misma implementación y la misma secuencia de llamadas, obtendrás la misma secuencia. Cambiar cualquiera de esos elementos puede modificar los valores posteriores.
Una semilla inicializa un generador; no es un número aleatorio por sí misma. Mantén separadas la conversión de una entrada fácil de usar, como una cadena, y la operación del algoritmo que actualiza el estado del generador. Los ejemplos siguientes usan PRNG pequeños de propósito general para simulaciones, demostraciones y pruebas repetibles.
Convertir una cadena en semillas para generadores
SFC32 y Xoshiro128** usan cuatro palabras de estado de 32 bits. Por eso, antes de inicializar esos generadores, hay que convertir una cadena en cuatro valores numéricos. Este hash compacto es una forma de hacerlo; no es un hash criptográfico y no debe usarse para proteger secretos. Como ocurre con cualquier hash de ancho fijo, distintas cadenas pueden producir el mismo resultado.
La función mezcla cada unidad de código UTF-16 en cuatro palabras sin signo de 32 bits. Su resultado sirve como estado determinista para los ejemplos, pero no añade entropía ni hace que el generador sea seguro. Usa la misma cadena y la misma función cada vez que necesites recrear el estado inicial.
El código y las etiquetas o salidas visibles de esta sección se mantienen en inglés.
function hashStringToWords(string) {
let a = 0x9e3779b9;
let b = 0x243f6a88;
let c = 0xb7e15162;
let d = 0xdeadbeef;
for (let i = 0; i < string.length; i++) {
const k = string.charCodeAt(i);
a = Math.imul(a ^ k, 0x85ebca6b);
b = Math.imul(b ^ k, 0xc2b2ae35);
c = Math.imul(c ^ k, 0x27d4eb2f);
d = Math.imul(d ^ k, 0x165667b1);
a ^= b >>> 13;
b ^= c >>> 11;
c ^= d >>> 15;
d ^= a >>> 9;
}
return [a, b, c, d].map((word) => word >>> 0);
}
const words = hashStringToWords("demo seed");
console.log(words.join(", "));
El generador también debe recibir el estado que requiere. SFC32 y Xoshiro128** reciben cuatro palabras; Mulberry32 recibe un número de 32 bits; el ejemplo de JSF que aparece a continuación expande una palabra en cuatro valores iniciales y precalienta el estado antes de devolver valores. No pases cuatro veces el mismo valor único suponiendo que todos los generadores se inicializarán correctamente.
Generar números con SFC32, Mulberry32, Xoshiro128** y JSF
Estas funciones devuelven valores en el intervalo de 0 (incluido) a 1 (excluido). SFC32 y Xoshiro128** reciben las cuatro palabras de hashStringToWords; Mulberry32 y JSF usan la primera palabra. Cada función conserva un estado mutable; crea un generador nuevo para reiniciar su secuencia.
SFC32 actualiza cuatro palabras de 32 bits en cada llamada, incluso si comienza con un estado compuesto solo por ceros. Xoshiro128** requiere un estado distinto de cero; para la entrada demo seed que se muestra, la función auxiliar de hash proporciona cuatro palabras distintas de cero.
El código y las etiquetas o salidas visibles de esta sección se mantienen en inglés.
function hashStringToWords(string) {
let a = 0x9e3779b9;
let b = 0x243f6a88;
let c = 0xb7e15162;
let d = 0xdeadbeef;
for (let i = 0; i < string.length; i++) {
const k = string.charCodeAt(i);
a = Math.imul(a ^ k, 0x85ebca6b);
b = Math.imul(b ^ k, 0xc2b2ae35);
c = Math.imul(c ^ k, 0x27d4eb2f);
d = Math.imul(d ^ k, 0x165667b1);
a ^= b >>> 13; b ^= c >>> 11; c ^= d >>> 15; d ^= a >>> 9;
}
return [a, b, c, d].map((word) => word >>> 0);
}
function sfc32(a, b, c, d) {
return function () {
a >>>= 0; b >>>= 0; c >>>= 0; d >>>= 0;
let t = (a + b) | 0;
a = b ^ (b >>> 9);
b = (c + (c << 3)) | 0;
c = (c << 21) | (c >>> 11);
d = (d + 1) | 0;
t = (t + d) | 0;
c = (c + t) | 0;
return (t >>> 0) / 4294967296;
};
}
const random = sfc32(...hashStringToWords("demo seed"));
console.log(random());
console.log(random());
Mulberry32 es un generador compacto que usa una sola palabra. La suma de una constante fija actualiza el estado antes de mezclar los bits para obtener cada resultado.
function mulberry32(seed) {
let state = seed >>> 0;
return function () {
let t = (state += 0x6d2b79f5);
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function hashStringToWords(string) {
let a = 0x9e3779b9;
for (let i = 0; i < string.length; i++) {
a = Math.imul(a ^ string.charCodeAt(i), 0x85ebca6b);
a ^= a >>> 13;
}
return a >>> 0;
}
const random = mulberry32(hashStringToWords("demo seed"));
for (let i = 0; i < 5; i++) console.log(random());
Xoshiro128** usa cuatro palabras y una transformación de salida star-star. La transición de estado es independiente del cálculo de la salida, así que conserva las cuatro palabras actualizadas entre llamadas.
function xoshiro128ss(s) {
s = s.map((word) => word >>> 0);
return function () {
const result = Math.imul(((Math.imul(s[1], 5) << 7) |
(Math.imul(s[1], 5) >>> 25)), 9) >>> 0;
const t = s[1] << 9;
s[2] ^= s[0];
s[3] ^= s[1];
s[1] ^= s[2];
s[0] ^= s[3];
s[2] ^= t;
s[3] = (s[3] << 11) | (s[3] >>> 21);
return result / 4294967296;
};
}
function hashStringToWords(string) {
let a = 0x9e3779b9, b = 0x243f6a88, c = 0xb7e15162, d = 0xdeadbeef;
for (let i = 0; i < string.length; i++) {
const k = string.charCodeAt(i);
a = Math.imul(a ^ k, 0x85ebca6b); b = Math.imul(b ^ k, 0xc2b2ae35);
c = Math.imul(c ^ k, 0x27d4eb2f); d = Math.imul(d ^ k, 0x165667b1);
a ^= b >>> 13; b ^= c >>> 11; c ^= d >>> 15; d ^= a >>> 9;
}
return [a, b, c, d].map((word) => word >>> 0);
}
const random = xoshiro128ss(hashStringToWords("demo seed"));
console.log(random());
JSF (Jenkins Small Fast) también mantiene cuatro palabras. Esta versión deriva el estado inicial de la primera palabra del hash, realiza 20 transiciones de estado antes de producir valores y luego devuelve un resultado de 32 bits escalado al mismo intervalo.
function jsf32(seed) {
let a = 0xf1ea5eed;
let b = seed >>> 0;
let c = seed >>> 0;
let d = seed >>> 0;
function nextUint32() {
const e = (a - ((b << 27) | (b >>> 5))) | 0;
a = b ^ ((c << 17) | (c >>> 15));
b = (c + d) | 0;
c = (d + e) | 0;
d = (a + e) | 0;
return d >>> 0;
}
for (let i = 0; i < 20; i++) nextUint32();
return () => nextUint32() / 4294967296;
}
function hashStringToWord(string) {
let a = 0x9e3779b9;
for (let i = 0; i < string.length; i++) {
a = Math.imul(a ^ string.charCodeAt(i), 0x85ebca6b);
a ^= a >>> 13;
}
return a >>> 0;
}
const random = jsf32(hashStringToWord("demo seed"));
for (let i = 0; i < 3; i++) console.log(random());
Los algoritmos producen secuencias distintas aunque se inicialicen con la misma cadena, porque sus transiciones de estado son diferentes. Para obtener resultados reproducibles, mantén fijo el algoritmo y el orden de las llamadas, además de la semilla.
Usar seedrandom.js
seedrandom.js es una biblioteca de JavaScript para generar números seudoaleatorios con semilla. El código de página para navegador que aparece a continuación carga la versión 3.0.5 desde CDNJS, crea un generador a partir de la cadena Johnson y registra su primer valor. El fragmento requiere que el navegador tenga acceso a la red para cargar el script; es código fuente que puedes adaptar, no un resultado verificado en tiempo de ejecución aquí.
El código y las etiquetas o salidas visibles de esta sección se mantienen en inglés.
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/3.0.5/seedrandom.min.js"></script>
<script>
const random = new Math.seedrandom("Johnson");
console.log(random());
</script>
</body>
Una instancia del generador también ofrece los métodos quick() e int32(). quick() devuelve un valor fraccionario; int32() devuelve un entero con signo de 32 bits. Cada llamada hace avanzar el generador, por lo que la segunda llamada no corresponde al mismo punto de la secuencia que la primera.
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/3.0.5/seedrandom.min.js"></script>
<script>
const random = new Math.seedrandom("DavidBau");
console.log("With quick():", random.quick());
console.log("With int32():", random.int32());
</script>
</body>
Elegir un comportamiento con semilla, mezclado con entropía o automático
Usa una semilla explícita cuando necesites recrear una secuencia a partir de la misma entrada. El ejemplo suministrado de seedrandom.js también muestra la opción { entropy: true } junto con una semilla. El ejemplo demuestra que se pasa esa opción; no establece una fuente de entropía concreta, una garantía ni una propiedad de repetibilidad, así que no te bases en él para hacer esas afirmaciones.
El código y las etiquetas o salidas visibles de esta sección se mantienen en inglés.
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/3.0.5/seedrandom.min.js"></script>
<script>
const random = new Math.seedrandom("Antananarivo", { entropy: true });
for (let i = 0; i < 5; i++) console.log(random());
</script>
</body>
Si necesitas valores nuevos en vez de una demostración repetible, elige una fuente adecuada para ese requisito y consulta su comportamiento documentado. Este artículo no presupone cómo se comportan los argumentos de semilla omitidos ni las opciones que no aparecen en las llamadas mostradas.
Límites de los números seudoaleatorios con semilla
Los PRNG con semilla son útiles cuando un programa necesita secuencias repetibles, por ejemplo, en una simulación o en los datos de prueba deterministas. Son algoritmos, no fuentes de secretos impredecibles. Si la semilla es predecible, la secuencia resultante también lo es; un hash de cadena personalizado no cambia ese hecho.
No uses estos ejemplos para generar contraseñas, tokens de autenticación, claves criptográficas ni otros valores sensibles desde el punto de vista de la seguridad. Para esos casos, usa una API de generación de números aleatorios criptográficamente segura y adecuada para el entorno de ejecución y la plataforma. Tampoco des por hecho que una semilla garantiza que dos implementaciones distintas de PRNG produzcan la misma secuencia: conserva el algoritmo, la conversión de la semilla, el estado inicial y el número y orden de las llamadas.