Cómo programar rarezas de objetos con probabilidades reales

Claim Your Loot

·


Quieres que tu cofre suelte objetos comunes casi siempre y un legendario de vez en cuando. Buscas cómo hacerlo y encuentras gente sumando if math.random(1,100) <= 5 then, encadenando ifs hasta que sale un bloque de treinta líneas que nadie entiende y que además no reparte lo que crees que reparte.

Aquí vas a montar el sistema que usan los juegos de verdad: una tabla con pesos y una función de seis líneas. Añadir una rareza nueva será escribir una línea más, no reescribir la lógica.

Respuesta rápida

Guarda cada objeto con un peso (un número cualquiera, no un porcentaje). Suma todos los pesos, saca un número aleatorio entre 0 y esa suma, y recorre la lista acumulando hasta pasarte. El objeto en el que te pasas es el premio. Añadir rarezas nuevas no toca la función.

Lo que necesitas antes de empezar

  • Roblox Studio con un lugar cualquiera abierto.
  • Saber dónde está ServerScriptService.
  • Unos 20 minutos.
  • No hace falta que sepas de probabilidad: la idea la vas a ver funcionando.

Por qué los ifs encadenados fallan

Este código lo ves por todas partes:

-- MAL: parece que funciona, pero no reparte lo que crees
local tirada = math.random(1, 100)

if tirada <= 60 then
	print("Común")
elseif tirada <= 25 then      -- esto NUNCA se ejecuta
	print("Poco común")
elseif tirada <= 10 then      -- esto tampoco
	print("Raro")
end

Si la tirada es 20, entra en el primer if (20 ≤ 60) y sale. Las ramas de abajo son inalcanzables: cualquier número menor que 25 ya era menor que 60. Acabas de hacer un cofre que da comunes el 60 % de las veces y nada el 40 % restante.

Para que funcione tendrías que escribir umbrales acumulados (60, 85, 95, 99, 100) y recalcularlos a mano cada vez que toques un valor. Es exactamente lo que vamos a evitar.

Paso 1: la tabla de rarezas

Crea un Script en ServerScriptService llamado SistemaDeRarezas:

-- Script en ServerScriptService
-- El peso es RELATIVO. No hace falta que sume 100.
local RAREZAS = {
	{nombre = "Común",      peso = 600, color = Color3.fromRGB(160, 160, 160)},
	{nombre = "Poco común", peso = 250, color = Color3.fromRGB(80, 200, 120)},
	{nombre = "Raro",       peso = 100, color = Color3.fromRGB(70, 150, 220)},
	{nombre = "Épico",      peso = 40,  color = Color3.fromRGB(160, 90, 230)},
	{nombre = "Legendario", peso = 10,  color = Color3.fromRGB(240, 180, 60)},
}

Los pesos suman 1.000, pero da igual: podrían sumar 37 o 4.281. Lo único que importa es la proporción. Eso es lo que hace que puedas subir un peso sin tocar los demás.

Usar números grandes (600 en vez de 60) tiene una ventaja práctica: si mañana quieres una rareza de un 0,3 %, le pones peso 3 y no tienes que multiplicar todo por diez.

Paso 2: la función que elige

-- Suma todos los pesos de la tabla
local function pesoTotal()
	local total = 0
	for _, r in ipairs(RAREZAS) do
		total = total + r.peso
	end
	return total
end

-- Elige una rareza respetando los pesos
local function elegirRareza()
	local tirada = math.random() * pesoTotal()
	local acumulado = 0

	for _, r in ipairs(RAREZAS) do
		acumulado = acumulado + r.peso
		if tirada <= acumulado then
			return r
		end
	end

	-- Red de seguridad: solo llegaría aquí por un redondeo raro
	return RAREZAS[1]
end

La idea en una frase: imagina una regla de 1.000 cm dividida en tramos. Los primeros 600 cm son «Común», los siguientes 250 «Poco común», y así. Tiras un dardo a un punto al azar de la regla y miras en qué tramo cayó. Eso es todo lo que hace el bucle.

math.random() sin argumentos devuelve un decimal entre 0 y 1, así que multiplicarlo por el total da un punto cualquiera de la regla.

Paso 3: comprueba que reparte bien

Esto es lo que casi nadie hace y lo que separa un sistema que funciona de uno que crees que funciona. Añade al final del script:

-- Prueba: 10.000 tiradas para ver si los porcentajes cuadran
local TIRADAS = 10000
local cuenta = {}

for _, r in ipairs(RAREZAS) do
	cuenta[r.nombre] = 0
end

for i = 1, TIRADAS do
	local r = elegirRareza()
	cuenta[r.nombre] = cuenta[r.nombre] + 1
end

print("--- Resultado de " .. TIRADAS .. " tiradas ---")
for _, r in ipairs(RAREZAS) do
	local esperado = r.peso / pesoTotal() * 100
	local real = cuenta[r.nombre] / TIRADAS * 100
	print(string.format("%-12s esperado %5.2f%%   real %5.2f%%   (%d)",
		r.nombre, esperado, real, cuenta[r.nombre]))
end

Dale a Play y mira el Output. Los porcentajes reales deben quedarse muy cerca de los esperados. Si alguna rareza sale 0 veces o el doble de lo que debería, tienes un fallo en la tabla.

Baja TIRADAS a 20 y vuelve a ejecutar. Vas a ver disparates: un 15 % de legendarios, cero raros. Eso no es un fallo, es cómo funciona el azar con pocas muestras, y es justo lo que va a experimentar cada jugador de tu juego por separado.

Paso 4: engancharlo a un objeto

Ya tienes la rareza. Ahora asigna objetos concretos a cada una:

local OBJETOS = {
	["Común"]      = {"Moneda de cobre", "Tabla de madera", "Cuerda"},
	["Poco común"] = {"Espada de hierro", "Escudo de roble"},
	["Raro"]       = {"Alas de tela", "Casco encantado"},
	["Épico"]      = {"Espada de fuego", "Montura veloz"},
	["Legendario"] = {"Corona del dragón"},
}

local function abrirCofre()
	local rareza = elegirRareza()
	local lista = OBJETOS[rareza.nombre]
	local objeto = lista[math.random(1, #lista)]
	return objeto, rareza
end

local objeto, rareza = abrirCofre()
print("Te ha tocado " .. objeto .. " (" .. rareza.nombre .. ")")

Separar «qué rareza» de «qué objeto dentro de esa rareza» te deja añadir objetos nuevos sin tocar las probabilidades. Es el mismo motivo por el que la función de elegir no sabe nada de objetos.

Compruébalo tú

¿Cuántas tiradas hacen falta?

Ejecuta la misma tabla de pesos con más o menos tiradas y mira cuándo empiezan a cuadrar los porcentajes.

Los mismos pesos que el script de arriba: 600 / 250 / 100 / 40 / 10

Errores habituales

Siempre sale la primera rareza

Estás usando math.random(1, pesoTotal()) con enteros y comparando mal, o has puesto < donde va <=. Con math.random() decimal y tirada <= acumulado funciona.

attempt to compare number with nil

Alguna entrada de la tabla no tiene peso. Suele pasar al añadir una rareza nueva copiando la línea de arriba y borrando el campo sin querer.

Los porcentajes no cuadran ni con 10.000 tiradas

Casi siempre es que estás llamando a pesoTotal() dentro del bucle con la tabla cambiando, o que tienes dos rarezas con el mismo nombre y una pisa a la otra en la tabla de cuentas.

En el juego publicado todos los jugadores sacan lo mismo

Estás sembrando el generador con math.randomseed(tick()) en cada llamada. No hace falta: Luau ya siembra bien al arrancar. Si lo haces tú y varios servidores arrancan en el mismo segundo, salen las mismas secuencias.

Preguntas frecuentes

¿Puedo poner un peso decimal?

Sí, peso = 0.5 funciona. Pero se lee peor. Es más limpio multiplicar toda la tabla por diez y usar enteros.

¿Cómo hago que un objeto sea más probable en un cofre premium?

Ten dos tablas con los mismos nombres de rareza y pesos distintos, y elige la tabla según el tipo de cofre. La función de elegir no cambia.

¿Esto se puede trampear desde el cliente?

Si el script está en ServerScriptService, no. Si lo pones en un LocalScript, cualquiera puede reescribir la tabla y darse legendarios. Las probabilidades se resuelven siempre en el servidor.

¿Y si quiero garantizar un legendario cada 100 cofres?

Eso es un contador de mala suerte, y merece su propio artículo porque cambia el sistema entero: hay que llevar la cuenta por jugador y subir el peso conforme sube la racha.


Última revisión: 1 de septiembre de 2026

Claim Your Loot

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *