Volver al Blog
Videojuegos20 de octubre de 202615 min de lectura

Aleatorio no es lo mismo que justo: cuándo el juego tiene que hacer trampa con el azar

Un 25 % de crítico puede fallar diez veces seguidas, y el jugador lo vivirá como un bug. Por qué los críticos necesitan pseudoaleatoriedad (PRD, shuffle bags, true hit) mientras que las cajas pueden seguir siendo puro azar.

IM
Ignacio MelendezDesarrollador Full-Stack y de Videojuegos
Aleatorio no es lo mismo que justo: cuándo el juego tiene que hacer trampa con el azar

Un personaje tiene un 25% de probabilidad de crítico. Un jugador lo prueba, ataca diez veces seguidas y no salta ni un solo crítico. Llega un reporte: el crítico está roto. Pero el sistema está funcionando correctamente. El problema es que, el jugador no ha experimentado que "uno de cada cuatro" ataques será crítico (aunque las matemáticas puedan decir lo contrario).

El azar de verdad tiene rachas, y los jugadores somos muy malos para aceptarlos. Hay sistemas que sí que pueden permitírselo, como por ejemplo abrir una caja y que no salga nada "legendario". Sin embargo, hay otros, como un golpe crítico, una probabilidad de esquiva de ataques o un 90% de acierto que falla consecutivamente. Se sienten como error del juego, no como mala suerte del jugador.

Por eso, muchos juegos hacen trampa con el azar a favor del jugador.

1. El azar y sus rachas

Un 25 % de crítico indica que la probabilidad de fallar diez veces seguidas es 0,7510≈5,6%0{,}75^{10} \approx 5{,}6\%. Las matemáticas indican que es posible que eso se cumpla, aunque suene anecdótico. En una partida, el jugador no ataca diez veces, sino cientos de ellas. Con 200 golpes, la probabilidad de que aparezca al menos una racha de diez fallos seguidos es del 96 %. No es que pueda pasar: es que va a pasar casi siempre.

El cerebro humano no suele asimilar bien las cuentas con porcentajes. De ahí nace la "falacia del jugador", que lleva a pensar que, después de muchos fallos, "ya toca" acertar, cuando cada tirada es independiente de la anterior. El sesgo también funciona al revés: dos críticos seguidos parecen mala suerte. Esperamos que el azar se reparta de forma uniforme a corto plazo, y el azar real hace justo lo contrario: se agrupa.

Un jugador no juzga la probabilidad, juzga la secuencia que vive y experimenta.

Sid Meier lo contó en su charla de la GDC de 2010 sobre Psicología del diseño de juegos. Cuando en Civilization un jugador perdía un combate con una ventaja de 3 a 1, sentía que el juego le había robado, aunque perder un 25 % de las veces fuera exactamente lo que decía el número. La conclusión de la charla es que el juego no tenía que ser fiel a las matemáticas, sino a la expectativa del jugador. A partir de ahí, el equipo empezó a corregir las tiradas a favor del jugador. Y poco a poco se fue acercando a lo que realmente se intuía como "azar justo".

Ejércitos de Civilization V enfrentados en un campo de hexágonos, con unidades de arqueros e infantería alrededor de un lago
En Civilization cada combate muestra sus probabilidades antes de atacar. Cuando una ventaja clara termina en derrota, el jugador culpa al juego, no al dado. Imagen: Firaxis Games / 2K.

2. PRD. Enseñanzas de Warcraft III y Dota 2

Warcraft III popularizó una técnica, que tiempo después heredó Dota 2, y que sigue usando para los golpes críticos y la mayoría de efectos que ocurren por probabilidad: la Distribución Pseudo-aleatoria (PRD, de Pseudo-Random Distribution). La idea detrás de esto es simple: la probabilidad no es fija, sino que sube con cada fallo y se reinicia cuando hay un acierto. La fórmula es:

P(N)=C×NP(N) = C \times N

Donde NN es el número de intentos desde el último acierto y CC es una constante. El primer ataque siempre tiene una probabilidad de CC, el segundo de 2C2C, el tercero de 3C3C, y así hasta que salta el crítico y el contador vuelve a 1. Si C×NC \times N llega a 1, el acierto está garantizado (sería lo mismo que forzar un 100% de probabilidad).

Lo importante es que CC no es la probabilidad que se refleja en el arma, la estadística o la descripción del hechizo. Para un crítico del 25 %, CC vale aproximadamente 0,0847 (utilizando la misma fórmula como base). Es decir, el primer ataque solo tiene un 8,5 % de probabilidad de que sea crítico, pero como la probabilidad va creciendo, a largo plazo acaba siendo exactamente del 25 %. Lo que cambia es el reparto: los críticos se espacian de forma mucho más regular. Una tabla para entenderlo mejor:

Probabilidad nominalConstante CCIntentos máximos sin acierto
10 %0,014768
15 %0,032232
25 %0,084712
30 %0,11899
50 %0,30214

Con un 25 %, la racha más larga posible es de 11 fallos, porque en el duodécimo intento ya va a forzar el efecto (12×0,0847>112 \times 0{,}0847 > 1). Con azar puro no hay techo y podrías intentarlo mil veces sin éxito (parece increíble pero las matemáticas así lo dicen).

CC no tiene una fórmula cerrada. Se calcula numéricamente: para un valor de CC se obtiene el número medio de intentos hasta un acierto, y se ajusta CC hasta que la inversa de esa media coincide con la probabilidad nominal. De esta forma, cambiar la probabilidad es tan sencillo como cambiar dicha constante.

La PRD también tiene un efecto secundario que Dota 2 aprovecha a propósito: castiga ligeramente al que provoca pocas "tiradas". Como el primer golpe tras un acierto tiene una probabilidad muy baja, no se puede llegar a una pelea y esperar hacer un crítico de inmediato. Hay que ir "cargandolo", de manera que el resultado dependa más de la constancia que de la suerte de un golpe aislado. Aunque, como hemos dicho varias veces, es posible que el jugador tenga suerte y el crítico llegue con el primer golpe.

Pelea en un carril de Dota 2 con Juggernaut girando con su espada entre creeps y héroes enemigos
En Dota 2 los críticos, esquivas y aturdimientos por probabilidad usan PRD. En una pelea así, nadie llega con el crítico ya cargado. Imagen: Valve.

3. Shuffle bag: la bolsa de 7 de Tetris

La PRD suaviza un evento de sí o no: el ataque es crítico, esquivas el golpe, el daño se reduce... Pero cuando lo que se va a calcular tiene varios resultados posibles, existe otra técnica más habitual: Shuffle Bag. Consisten en que, en lugar de tirar un dado cada vez, se agrupan todos en una "bolsa", la agitas y mezclas, y se van sacando resultados sin meter de nuevo el dado. Cuando se vacía, se vuelve a llenar y comienza el proceso de nuevo.

Uno de los mejores ejemplos es Tetris. Las primeras versiones elegían cada pieza de forma independiente, así que el jugador podía poner treinta piezas antes de ver una barra larga (la I). No fue hasta principios de los 2000, que Tetris implementó el generador conocido como 7-bag: las siete piezas entran en una bolsa, se barajan y salen una a una. De esta forma, cada siete piezas el jugador ve cada tipo de pieza, sin excepción.

Partida de Tetris Effect: Connected con una pieza L cayendo, una barra larga en reserva y un cuadrado como siguiente pieza
Los Tetris modernos, como Tetris Effect, enseñan la siguiente pieza y permiten guardar una. Con la bolsa de 7, la barra larga nunca tarda más de 12 piezas en volver. Imagen: Monstars y Resonair / Enhance.

Con esta estrategia se garantiza que, entre dos pieza iguales, hay como mucho 12 piezas de otro tipo (cuando una sale la primera de una bolsa y la siguiente, la última de la bolsa siguiente). También se consigue que nunca salgan más de cuatro S o Z seguidas, que son las más difíciles de colocar correctamente. Sigue existiendo sensación de aleatoriedad, ya que el jugador no sabe qué viene ni en qué orden llegarán, pero ya no puede tener una partida arruinada por la "mala suerte".

Una shuffle bag también sirve para eventos de sí o no. Una bolsa con 1 acierto y 3 fallos da un 25 % exacto en cada tanda de cuatro intentos. Es más rígida que la PRD (el jugador atento puede contar y por lo tanto, "trucar su suerte"), pero va muy bien para cosas como diálogos de NPC o efectos de sonido, donde repetir el mismo resultado dos veces seguidas queda feo.

4. Fire Emblem y el "true hit"

En el artículo sobre los growth rates de Fire Emblem comenté cómo una sola tirada de 0 a 99 decide si una estadística sube. Para los ataques, la saga Fire Emblem implementó algo más astuto (desde Game Boy Advance): en lugar de una tirada, hace dos y usa la media. Es lo que la comunidad llama true hit o sistema 2RN.

El efecto que consigue es que los porcentajes altos son más altos de lo que dicen ser, y, por extensión, los bajos, más bajos. La media de dos tiradas se concentra alrededor de 50, así que los valores de los extremos pierden peso. Algo parecido sería:

Acierto mostradoAcierto real (2RN)
20 %8,2 %
30 %18,3 %
50 %50,5 %
70 %82,3 %
80 %92,2 %
90 %98,1 %

Esto es lo que Sid Meier describía como "el juego ajusta la probabilidad a lo que un jugador espera que signifique". Por ejemplo, un 80 % que se comporta como un 92 %.

Ocurre lo mismo con los enemigos. Contra unidades del jugador bien posicionadas, los porcentajes de esquiva son bajos y el sistema hace que eviten menos golpes que "no deberían" fallar.

El true hit es la técnica más "deshonesta" de las que aparecen aquí, porque el número de la interfaz no es el real. Funciona en Fire Emblem porque el desvío siempre favorece al jugador y casi nadie lo nota. Si el desvío jugara en contra, sería indefendible en cuanto alguien lo midiera, y en juegos con comunidades activas siempre hay alguien que lo mide.

5. Azar en cajas y drops

Con los críticos, la racha es el enemigo. En cambio, con las cajas, los sobres y los drops de jefes y enemigos la sorpresa es el producto. Si cada veinte sobres, el objeto legendario estuviera garantizado, los diecinueve sobres anteriores dejan de tener importancia y serían un mero trámite. Lo que hace emocionante abrir un sobre es que el objeto deseado (generalmente el de mayor rareza y baja probabilidad) puede salir cada vez.

En estos casos no se suaviza la distribución, sino que se corta la cola. Hearthstone es el ejemplo clásico: cada sobre tiene una probabilidad de carta legendaria, pero si el jugador lleva 40 sobres sin tocarle ninguna, el siguiente la trae garantizada. Ninguna parte del juego lo indica. De echo, en este caso, fue la comunidad que lo descubrió tras analizar miles de aperturas y compararlas. Genshin Impact hace lo mismo con su gacha: 0,6 % por tirada, con la probabilidad disparándose a partir de la tirada 74 (lo que se conoce como soft pity) y un 5 estrellas garantizado en la 90 (llamada hard pity).

Sin ese límite, un 0,6 % durante 90 tiradas solo da un 41,8 % de probabilidad de conseguir algo bueno. Lo que se traduciría en que una gran parte de los jugadores se quedaría sin una recompensa buena después de hacer un gasto considerable. El "pity" no cambia lo que vive el jugador medio, aquel que abre un sobre o hace una tirada de vez en cuando y sigue jugando. Esta estrategia solo protege al que más mala suerte tiene, que es justo el jugador que más probabilidades tiene de dejar el juego por frustración.

En un crítico la racha es un bug pero en una caja la sorpresa es el producto. El pity no la elimina, solo pone un límite a la mala suerte.

6. Cuándo ayudar al azar

Después de ver los cuatro casos, la decisión se reduce a estas preguntas sobre el evento:

  1. ¿Con qué frecuencia se hace una tirada? Si ocurre decenas de veces por minuto (críticos, esquivas, procs), el jugador llega a percibir la secuencia y las rachas se acaban notando. Si se tira una vez por sesión (un drop de jefe), cada tirada se vive por separado y son experiencias distintas por cada jugador y partida.
  2. ¿El jugador ha tomado una decisión que dependa del resultado? Si ha preparado una build de críticos o ha colocado a su unidad para el 80 %, una racha mala castiga su decisión y puede sentir que ha perdido el tiempo. Si solo abre un sobre, no hay decisión que castigar.
  3. ¿Fallar se siente como castigo o como ausencia de premio? Fallar un ataque puede costarle la partida. Que no salga la legendaria solo significa que no ganó algo raro o extra.
  4. ¿El jugador puede llegar a ver las rachas? Un contador de críticos en pantalla o un historial de tiradas hace que cualquier agrupación salte a la vista. Un drop que nadie registra no genera sensación de injusticia.

Si las respuestas apuntan a "a menudo, con decisión, castigo, visible", conviene suavizar toda la distribución con PRD o shuffle bag. Si apuntan a "poco, sin decisión, premio, invisible", lo mejor es azar puro, con pity si el caso extremo es muy doloroso. Y el true hit queda para cuando se quiere que el número mostrado signifique lo que el jugador cree que significa, aceptando que es una mentira.

7. Implementación en Unity y Unreal

Al implementarlo, la abstracción es una interfaz mínima, IRandomSource, que responde a una sola pregunta: ¿ha salido esta vez? Encima de ella hay cuatro implementaciones intercambiables: azar puro, PRD, shuffle bag y un pity counter que envuelve a cualquiera de las anteriores. El código de gameplay depende de la interfaz, así que cambiar un crítico de azar puro a PRD es cambiar una línea.

En Unity, cada fuente recibe un IRng del artículo sobre RNG en roguelikes, así que hereda los streams con seed y la reproducibilidad. En Unreal, el equivalente nativo es FRandomStream, que ya es un generador con seed propia.

1public interface IRandomSource
2{
3    bool Roll();
4    void Reset();
5}
6
7public sealed class TrueRandom : IRandomSource
8{
9    private readonly IRng _rng;
10    private readonly float _probability;
11
12    public TrueRandom(IRng rng, float probability)
13    {
14        _rng = rng;
15        _probability = probability;
16    }
17
18    public bool Roll() => _rng.Chance(_probability);
19
20    public void Reset() { }
21}

PRD y el cálculo de la constante

La PRD necesita la constante CC. El cálculo se puede hacer con una búsqueda binaria: para un CC candidato, se suma el número esperado de intentos hasta el primer acierto y se compara su inversa con la probabilidad buscada. Si sale baja, se sube CC; si sale alta, se baja. Cincuenta iteraciones bastan para tener precisión de sobra.

1public static class PrdConstant
2{
3    public static float For(float probability)
4    {
5        if (probability <= 0f) return 0f;
6        if (probability >= 1f) return 1f;
7
8        double low = 0, high = probability;
9        for (int i = 0; i < 50; i++)
10        {
11            double mid = (low + high) / 2;
12            if (ExpectedRate(mid) < probability) low = mid;
13            else high = mid;
14        }
15        return (float)((low + high) / 2);
16    }
17
18    // Probabilidad media resultante de usar P(N) = C * N
19    private static double ExpectedRate(double c)
20    {
21        double expectedAttempts = 0, noHitYet = 1;
22        for (int n = 1; noHitYet > 0; n++)
23        {
24            double p = System.Math.Min(1, c * n);
25            expectedAttempts += n * noHitYet * p;
26            noHitYet *= 1 - p;
27        }
28        return 1 / expectedAttempts;
29    }
30}
31
32public sealed class PseudoRandomDistribution : IRandomSource
33{
34    private readonly IRng _rng;
35    private readonly float _c;
36    private int _attempts;
37
38    public PseudoRandomDistribution(IRng rng, float probability)
39    {
40        _rng = rng;
41        _c = PrdConstant.For(probability);
42    }
43
44    public bool Roll()
45    {
46        _attempts++;
47        if (_rng.Chance(Mathf.Min(1f, _c * _attempts)))
48        {
49            _attempts = 0;
50            return true;
51        }
52        return false;
53    }
54
55    public void Reset() => _attempts = 0;
56}

Se calcula CC una vez, al crear la fuente, y nunca en cada tirada. Si la probabilidad de crítico cambia en mitad de la partida (un objeto, un buff), se debe crear una fuente nueva o recalcular CC en ese momento. Si hay muchas probabilidades distintas, también se puede precalcular una tabla en el editor y guardarla como asset.

Shuffle bag y pity counter

La shuffle bag guarda una lista de aciertos y fallos, la baraja con Fisher-Yates y la va consumiendo. El pity counter es un decorador: envuelve otra fuente y fuerza un acierto si se alcanza el límite de fallos. Lo normal es combinarlo con TrueRandom para los drops.

1public sealed class ShuffleBag : IRandomSource
2{
3    private readonly IRng _rng;
4    private readonly List<bool> _bag = new();
5    private int _index;
6
7    public ShuffleBag(IRng rng, int hits, int size)
8    {
9        _rng = rng;
10        for (int i = 0; i < size; i++) _bag.Add(i < hits);
11        Reset();
12    }
13
14    public bool Roll()
15    {
16        if (_index >= _bag.Count) Reset();
17        return _bag[_index++];
18    }
19
20    public void Reset()
21    {
22        _rng.Shuffle(_bag);
23        _index = 0;
24    }
25}
26
27public sealed class PityCounter : IRandomSource
28{
29    private readonly IRandomSource _inner;
30    private readonly int _hardPity;
31
32    public int Misses { get; private set; }
33
34    public PityCounter(IRandomSource inner, int hardPity)
35    {
36        _inner = inner;
37        _hardPity = hardPity;
38    }
39
40    public bool Roll()
41    {
42        if (Misses + 1 >= _hardPity)
43        {
44            Reset();
45            return true;
46        }
47
48        bool hit = _inner.Roll();
49        Misses = hit ? 0 : Misses + 1;
50        return hit;
51    }
52
53    public void Reset()
54    {
55        Misses = 0;
56        _inner.Reset();
57    }
58}

Exponer Misses no es un capricho. Muchos juegos con gacha enseñan el contador de pity en la interfaz. Además, aunque no se enseñe, es interesante guardarlo en la partida, de manera que si el jugador cierra el juego en el sobre 39, el 40 tiene que seguir siendo el garantizado cuando vuelva a jugar.

El estado de la PRD, la bolsa y el pity forma parte de la partida. Si no se serializa junto al estado del RNG (como se explica en el artículo de roguelikes), cargar una partida reinicia las rachas y se rompe la reproducibilidad: misma seed, distintos críticos.

Usarlo en gameplay

Con todo montado, el código de combate no sabe qué tipo de azar usa. En Unity, el componente pide su stream a la RngFactory y crea la fuente en Awake. En Unreal, un UActorComponent deriva su FRandomStream de la seed raíz con la misma técnica de XOR por dominio, y crea la fuente en BeginPlay. De esta forma, todo se abstrae, y se puede actualizar en cualquier momento sin tener que "rehacer" todo.

1public class CritSystem : MonoBehaviour
2{
3    [SerializeField, Range(0f, 1f)] private float critChance = 0.25f;
4    [SerializeField] private float critMultiplier = 2f;
5
6    private IRandomSource _crit;
7
8    public void Init(RngFactory rngFactory)
9    {
10        IRng combatRng = rngFactory.For(RngDomain.Combat);
11        _crit = new PseudoRandomDistribution(combatRng, critChance);
12    }
13
14    public int ResolveDamage(int baseDamage) =>
15        _crit.Roll() ? Mathf.RoundToInt(baseDamage * critMultiplier) : baseDamage;
16}
17
18public class LootTable
19{
20    private readonly IRandomSource _legendary;
21
22    public LootTable(RngFactory rngFactory)
23    {
24        IRng lootRng = rngFactory.For(RngDomain.LootDrops);
25        _legendary = new PityCounter(new TrueRandom(lootRng, 0.05f), hardPity: 40);
26    }
27
28    public bool RollLegendary() => _legendary.Roll();
29}

En C# se añade Combat al enum RngDomain del artículo anterior. Así los críticos no consumen números del stream de botín, y añadir un efecto de partículas no cambia qué golpes hacen crítico. En Unreal, ERngDomain es la misma enumeración declarada como enum class. Desde Blueprints de Unreal, envuelve la interfaz en un UObject o expón solo métodos como ResolveDamage, como en el ejemplo.

8. Simulación: azar puro contra PRD

Para comprobar que todo esto cambia de verdad la experiencia, simulé un millón de ataques con un 25 % de crítico usando las dos fuentes. Las dos dan prácticamente la misma media (25,1 % con azar puro y 25,0 % con PRD), pero las rachas de fallos entre crítico y crítico son muy distintas:

Fallos entre dos críticosAzar puroPRD
0 (críticos consecutivos)25,2 %8,5 %
1 a 232,8 %34,9 %
3 a 524,5 %46,1 %
6 a 912,1 %10,5 %
10 a 144,2 %0,05 %
15 o más1,3 %0 %
Racha más larga4411

La PRD concentra casi la mitad de los casos en el rango de 3 a 5 fallos, que es lo que el jugador espera de "uno de cada cuatro". Desaparecen los dos extremos que generan quejas: los críticos encadenados (que con azar puro son uno de cada cuatro) y las rachas de diez o más fallos (más del 5 % con azar puro, prácticamente cero con PRD). Con azar puro, en un millón de ataques hubo una racha de 44 fallos seguidos. Con PRD, el máximo fue 11, justo el límite teórico de la tabla de la sección 2.

Conclusiones

  • El azar puro tiene rachas y el jugador las juzga por la secuencia que vive, no por la probabilidad nominal. Con un 25 % y 200 tiradas, una racha de diez fallos es casi segura.
  • La PRD de Warcraft III y Dota 2 mantiene la media y elimina las rachas extremas haciendo crecer la probabilidad con cada fallo. La constante CC se calcula numéricamente una sola vez.
  • La shuffle bag garantiza el reparto en cada tanda, como la bolsa de 7 de Tetris. Es más rígida que la PRD, pero perfecta cuando no se quieren repeticiones.
  • El true hit de Fire Emblem cambia la probabilidad real para que coincida con la percibida. Funciona porque siempre favorece al jugador.
  • En cajas y drops, la sorpresa es el producto. No hay que suavizar la distribución. Se puede utilizar un pity que solo recorte el peor caso.
  • Para decidir, hay que preguntarse con qué frecuencia se tira, si hay una decisión del jugador detrás, si fallar castiga o solo no premia y si las rachas se ven.
  • Implementar todo detrás de una interfaz como IRandomSource permite cambiar de técnica sin tocar el gameplay, y conectarla a streams con seed mantiene la reproducibilidad.

Artículos Relacionados

Ver todos los artículos
Cómo funciona el RNG en roguelikes

Cómo funciona el RNG en roguelikes

Los roguelikes dependen del azar, pero ese azar tiene estructura. Este artículo explora seeds, determinismo y cómo implementar un sistema de RNG con seed en Unity/C#.

Los stats de Fire Emblem y la ilusión de profundidad táctica

Los stats de Fire Emblem y la ilusión de profundidad táctica

Los growth rates de Fire Emblem son simples tiradas de porcentaje, pero generan una varianza brutal, apego emocional y subculturas enteras de manipulación de guardados.