¿Cómo funciona realmente lo random en JavaScript?

¿Cómo funciona realmente lo random en JavaScript?

Cuando escribimos:

Math.random()

parece que el lenguaje simplemente "saca un número del azar".

Pero una computadora no puede decidir espontáneamente qué número aparecerá. Una computadora ejecuta instrucciones deterministas: recibe un estado, realiza operaciones y obtiene otro estado.

Por eso el concepto importante es este:

> Un generador pseudoaleatorio produce una secuencia de números mediante un algoritmo determinista que está diseñada para tener apariencia de aleatoriedad.

Para entenderlo hay que dejar de pensar en Math.random() como una función mágica y visualizar lo que existe debajo.

1. Una computadora no necesita inventar un número

Supongamos que queremos producir una secuencia de números:

17
43
82
11
95

Una forma extremadamente simplificada de pensar el proceso sería:

valor actual
     ↓
operaciones matemáticas
     ↓
nuevo valor
     ↓
operaciones matemáticas
     ↓
otro valor
     ↓
...

El punto importante es que cada valor depende del estado anterior.

No existe necesariamente un proceso como:

"elige cualquier número que quieras"

Existe algo parecido a:

estado actual -> algoritmo -> siguiente estado

2. El estado interno

Un generador pseudoaleatorio necesita conservar información interna. A esa información podemos llamarla estado.

Por ejemplo, imaginemos un generador extremadamente sencillo cuyo estado inicial sea:

estado = 7

Ahora definimos una regla matemática:

nuevo_estado = (estado * 5 + 3) mod 16

Esto significa que para obtener el siguiente estado tomamos el actual, hacemos una multiplicación, una suma y después usamos el operador módulo.

Partiendo de:

estado = 7

obtenemos:

(7 * 5 + 3) mod 16
= 38 mod 16
= 6

El nuevo estado es:

6

Volvemos a aplicar la misma regla:

(6 * 5 + 3) mod 16
= 33 mod 16
= 1

Ahora el estado es:

1

Después:

(1 * 5 + 3) mod 16
= 8

La secuencia sería:

7 -> 6 -> 1 -> 8 -> ...

No hay magia.

Existe una regla que transforma un estado en otro estado.

3. Entonces, ¿por qué parece aleatorio?

Porque una función matemática puede producir una secuencia difícil de reconocer a simple vista.

Por ejemplo, un generador puede producir algo como:

0.728491
0.105238
0.913742
0.381029
0.667291

Para una persona parece azar.

Pero internamente existe una relación matemática entre un estado y el siguiente.

En términos simplificados:

Estado A
   ↓
Algoritmo
   ↓
Estado B
   ↓
Algoritmo
   ↓
Estado C

Por eso se utiliza el término pseudoaleatorio.

Es una secuencia generada mediante un procedimiento determinista que produce propiedades similares al azar para determinados usos.

4. La semilla

Ahora aparece otra pieza importante: la semilla, conocida como seed.

La semilla es el valor inicial utilizado para establecer el estado inicial del generador.

Podemos imaginar:

seed = 7

A partir de esa semilla comienza la secuencia:

7 -> 6 -> 1 -> 8 -> 11 -> ...

Si volvemos a comenzar con la misma semilla y utilizamos exactamente el mismo algoritmo, obtenemos la misma secuencia.

Por ejemplo:

seed = 7

7 -> 6 -> 1 -> 8 -> 11

Otra ejecución:

seed = 7

7 -> 6 -> 1 -> 8 -> 11

Esto demuestra algo fundamental:

> Un generador pseudoaleatorio no está improvisando resultados; está recorriendo una secuencia determinada por su estado inicial y su algoritmo.

5. ¿Entonces Math.random() funciona así exactamente?

No debemos asumir que JavaScript utiliza exactamente el ejemplo matemático anterior.

Ese ejemplo solamente sirve para entender el concepto.

La especificación de JavaScript define el comportamiento observable de Math.random(), pero no obliga a los motores a utilizar un algoritmo pseudoaleatorio específico.

Por eso distintos motores de JavaScript pueden utilizar diferentes implementaciones internas.

Lo importante conceptualmente es que el motor mantiene algún mecanismo interno capaz de producir valores pseudoaleatorios y devuelve un resultado que se encuentra en el rango requerido por JavaScript.

Por ejemplo:

const x = Math.random();

produce conceptualmente un valor:

0 <= x < 1

6. El número entre 0 y 1 es una representación

Supongamos que internamente un generador produce un valor entero grande:

381274921

Ese valor puede transformarse matemáticamente en una fracción para obtener un valor dentro del rango:

0 <= x < 1

De manera simplificada podemos imaginar:

valor entero
     ↓
división o normalización
     ↓
valor decimal
     ↓
0.0 ... 0.9999...

No debemos interpretar que JavaScript simplemente toma cualquier número del mundo y lo mete entre 0 y 1. Existe un procedimiento interno para producir la representación final que recibe el programador.

7. Cómo pasamos de un valor entre 0 y 1 a otro rango

Ahora supongamos que obtenemos:

0.73

Pero necesitamos un valor entre 0 y 100.

Multiplicamos:

0.73 * 100 = 73

En general:

random * rango

transforma el intervalo aproximado:

0 <= random < 1

en:

0 <= resultado < rango

Por ejemplo:

const x = Math.random();
const resultado = x * 100;

Si x fuera:

0.214

entonces:

0.214 * 100 = 21.4

8. ¿Por qué aparece Math.floor()?

Porque muchas veces queremos un entero y no un decimal.

Supongamos:

21.4

Math.floor() toma el entero inferior:

Math.floor(21.4)

21

Por eso esta estructura es tan común:

Math.floor(Math.random() * 100);

Conceptualmente ocurre:

Generador pseudoaleatorio
        ↓
0 <= x < 1
        ↓
Multiplicar por 100
        ↓
0 <= x < 100
        ↓
Math.floor()
        ↓
0 ... 99

Para obtener 1 ... 100, desplazamos el intervalo:

Math.floor(Math.random() * 100) + 1;

Ahora:

0 ... 99

se convierte en:

1 ... 100

9. Lo verdaderamente importante: un índice aleatorio

Aquí comienza a conectarse todo.

Imaginemos este arreglo:

const letras = ["A", "B", "C", "D", "E"];

Tiene cinco elementos.

Sus posiciones son:

índice

0 -> A
1 -> B
2 -> C
3 -> D
4 -> E

El generador nos entrega un valor entre 0 y 1.

Supongamos:

0.62

Multiplicamos por 5:

0.62 * 5 = 3.1

Aplicamos floor:

3

Ahora usamos 3 como índice:

letras[3]

Resultado:

D

La computadora no dijo:

> "Quiero la letra D."

El proceso fue:

estado interno
      ↓
algoritmo pseudoaleatorio
      ↓
valor pseudoaleatorio
      ↓
0.62
      ↓
0.62 * 5
      ↓
3.1
      ↓
floor
      ↓
3
      ↓
letras[3]
      ↓
D

Esta cadena de transformaciones es la idea fundamental.

10. Cómo se genera una letra random

Supongamos:

const letras = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

Internamente una cadena puede ser accedida mediante posiciones:

0 -> A
1 -> B
2 -> C
3 -> D
...
25 -> Z

Entonces necesitamos obtener un índice entre 0 y 25.

El proceso conceptual es:

Math.random()
      ↓
valor entre 0 y 1
      ↓
× 26
      ↓
valor entre 0 y menor que 26
      ↓
floor
      ↓
0 ... 25
      ↓
letras[indice]
      ↓
carácter

Por ejemplo:

0.41
 ↓
0.41 * 26
 ↓
10.66
 ↓
floor
 ↓
10
 ↓
letras[10]
 ↓
K

11. Cómo se construye un texto random

Un texto aleatorio no requiere un algoritmo completamente diferente.

Podemos utilizar el mecanismo anterior muchas veces.

Supongamos que queremos ocho caracteres.

iteración 1 -> índice 7  -> H
iteración 2 -> índice 2  -> C
iteración 3 -> índice 19 -> T
iteración 4 -> índice 4  -> E
iteración 5 -> índice 11 -> L
iteración 6 -> índice 0  -> A
iteración 7 -> índice 22 -> W
iteración 8 -> índice 14 -> O

Entonces construimos:

HCTELAWO

El código podría verse así:

const caracteres = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
let resultado = "";

for (let i = 0; i < 8; i++) {
    const indice = Math.floor(Math.random() * caracteres.length);
    resultado += caracteres[indice];
}

console.log(resultado);

El for no genera aleatoriedad.

El for solamente repite el proceso.

La aleatoriedad viene de la generación pseudoaleatoria del índice.

12. Cómo se construye una contraseña random

Una contraseña es exactamente la misma idea, pero el conjunto de posibilidades es más grande.

Por ejemplo:

const caracteres = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*";

Ahora tenemos letras mayúsculas, minúsculas, números y símbolos.

Para cada posición de la contraseña hacemos:

generar valor pseudoaleatorio
        ↓
convertirlo en un índice
        ↓
obtener carácter
        ↓
agregar carácter

Si queremos diez caracteres:

posición 1 -> M
posición 2 -> 7
posición 3 -> q
posición 4 -> @
posición 5 -> Z
posición 6 -> 3
posición 7 -> b
posición 8 -> #
posición 9 -> P
posición 10 -> 8

Resultado:

M7q@Z3b#P8

De nuevo, la computadora no construye una contraseña de una sola operación.

Construye cada posición individualmente.

13. El papel del algoritmo pseudoaleatorio

Aquí está la parte más profunda.

Imaginemos un algoritmo hipotético:

SiguienteEstado = (EstadoActual * A + B) mod M

Donde:

EstadoActual -> estado actual del generador
A            -> constante matemática
B            -> constante matemática
M            -> tamaño del espacio de estados

Un generador real puede ser mucho más sofisticado, pero conceptualmente sigue existiendo una transformación del estado.

Podemos visualizarlo así:

          ┌──────────────────────┐
          │    Estado actual     │
          └──────────┬───────────┘
                     ↓
             Algoritmo PRNG
                     ↓
          ┌──────────────────────┐
          │    Nuevo estado      │
          └──────────┬───────────┘
                     ↓
             representación
                     ↓
             valor devuelto

La secuencia puede continuar mientras el generador siga actualizando su estado.

14. ¿Por qué eventualmente puede repetirse?

Porque el estado interno tiene un espacio finito.

Supongamos que nuestro generador solamente puede tener estados:

0, 1, 2, 3

Solamente existen cuatro estados posibles.

Si continuamos aplicando una función que siempre transforma un estado en otro, eventualmente tendremos que volver a un estado que ya apareció.

Por ejemplo:

0 -> 2 -> 3 -> 1 -> 0 -> 2 -> 3 -> 1 -> ...

La secuencia comenzó nuevamente cuando apareció 0.

Por eso hablamos de un período.

El período indica cuántos estados pueden recorrer antes de empezar a repetirse.

Los generadores modernos utilizados para propósitos generales intentan tener períodos extremadamente grandes y buenas propiedades estadísticas.

15. Random no significa seguridad

Que una secuencia parezca aleatoria no significa que sea apropiada para seguridad.

Un generador pseudoaleatorio puede ser excelente para:

juegos
simulaciones
selecciones
animaciones
datos de prueba

pero un atacante podría ser capaz de predecir estados o resultados si el generador no está diseñado para criptografía.

Por eso no debemos asumir que:

Math.random()

sirve para generar contraseñas reales, tokens de autenticación o claves secretas.

Para seguridad existen generadores criptográficamente seguros, como los mecanismos proporcionados mediante la API crypto.

La idea fundamental

Cuando ves:

Math.random()

no deberías imaginar:

MAGIA
 ↓
NÚMERO ALEATORIO

Una forma mucho mejor de imaginarlo es:

Estado interno
      ↓
Algoritmo matemático
      ↓
Nuevo estado
      ↓
Valor pseudoaleatorio
      ↓
Conversión al formato necesario

Después ese valor puede transformarse.

Para un número:

valor pseudoaleatorio
      ↓
escalamiento
      ↓
redondeo o truncamiento
      ↓
entero

Para una letra:

valor pseudoaleatorio
      ↓
× cantidad de caracteres
      ↓
floor
      ↓
índice
      ↓
carácter

Para una contraseña:

valor pseudoaleatorio
      ↓
índice
      ↓
carácter
      ↓
repetir muchas veces
      ↓
cadena final

Por eso generar una contraseña random no es un concepto diferente de generar una letra random.

Es el mismo mecanismo aplicado repetidamente.

La cadena completa de pensamiento

Podemos resumir todo el proceso de esta manera:

SEED / ESTADO INICIAL
        ↓
ALGORITMO PSEUDOALEATORIO
        ↓
NUEVO ESTADO
        ↓
VALOR PSEUDOALEATORIO
        ↓
NORMALIZACIÓN
        ↓
NÚMERO ENTRE 0 Y 1
        ↓
TRANSFORMACIÓN AL RANGO NECESARIO
        ↓
ÍNDICE
        ↓
ELEMENTO

Y dependiendo de lo que queramos obtener:

ÍNDICE -> número
ÍNDICE -> letra
ÍNDICE -> palabra
ÍNDICE -> color
ÍNDICE -> elemento de un arreglo
ÍNDICE -> carácter de una contraseña

La verdadera lección no es aprender una línea como:

Math.floor(Math.random() * 10)

La verdadera lección es entender por qué esa línea funciona.

Math.random() produce un valor pseudoaleatorio dentro de un rango. Multiplicar cambia la escala del rango. Math.floor() convierte esa posición en un entero. Finalmente, ese entero puede utilizarse como índice para seleccionar una posibilidad.

Así es como una operación matemática aparentemente simple puede terminar produciendo un número, una letra, una palabra o una contraseña.

© Arturo Juárez Monroy