RollEngine is a reusable Roblox probability engine where rolling, pity, modifiers, odds display, and testing all use the same rules and state model.
Define rarities, weighted items, and optional pity rules once. For each request, your game supplies the current state, modifiers, and random source, then receives the selected result and a fresh NextState to save.
Get RollEngine
- Get the source, documentation, and downloadable model from the RollEngine GitHub repository.
- Or open the RollEngine Roblox place.
See it in use
RollEngine is used in Delinquent Online.
It has no DataStore, currency, reward, UI, or Player dependency, so the same engine can power:
- Loot crates and enemy drops
- Pets, eggs, and companions
- Character, class, style, and trait rolls
- Fishing, mining, and gathering rewards
- Cosmetics and event rewards
- Random encounters or procedural choices
Why use it?
Random reward systems often begin as a few math.random() calls. They become difficult to maintain once luck boosts, pity, weighted items, odds displays, saving, and tests are added.
RollEngine keeps that probability logic in one place:
- One source of truth:
Roll()andGetOdds()use the same distribution, including pity, modifiers, exclusions, item weights, and normalization. - Transaction-friendly: the supplied state is never mutated. Commit the cost, reward, and returned
NextStatetogether, or discard the attempted transaction. - Pity and temporary modifiers: add soft pity, one hard-pity rule, and multiple named counters. Target luck boosts, events, exclusions, and other changes by ID or reusable group tag.
- Exact next-roll odds:
GetOdds()reports the exact distribution for the supplied state and modifiers without consuming randomness. - Deterministic testing: the same rules, state, modifiers, and random draw values produce the same result, which helps reproduce bugs and boundary cases.
- Fail-fast validation: malformed static rules are rejected when the engine is created, while invalid roll requests return structured errors.
- Balance tooling: empirical simulation measures long-run behavior, while AutoTune searches for an approximate multiplier to investigate and verify.
- Reusable two-level weighting: configure rarity weights and independent item weights once, then use the engine for loot, pets, traits, encounters, and more.
RollEngine owns only its compiled rules. It has no background tasks, Player connections, hidden player-state tables, or global random source. It decides what was rolled, while your game handles permission, cost, rewards, persistence, and presentation.
A complete small example
In this example, RollEngine is a child of the server Script.
--!strict
local RollEngine = require(script.RollEngine)
local LootEngine = RollEngine.new({
Rarities = {
{
Id = "Common",
Weight = 75,
Items = {
{ Id = "Coins", Weight = 3 },
{ Id = "Potion", Weight = 1 },
},
},
{
Id = "Rare",
Weight = 20,
Items = {
{ Id = "Sword", Weight = 3 },
{ Id = "Cape", Weight = 1 },
},
},
{
Id = "Legendary",
Weight = 5,
Tags = { "LuckAffected" },
Items = {
{ Id = "Dragon", Weight = 1 },
},
},
},
PityRules = {
{
Id = "LegendaryPity",
ResetWhen = { RarityIds = { "Legendary" } },
HardPity = {
AtRoll = 10,
ForceRarityId = "Legendary",
},
},
},
})
local State = LootEngine:GetDefaultState()
local RandomGenerator = Random.new()
local Response = LootEngine:Roll({
State = State,
Random = RandomGenerator,
})
if Response.Success then
local Result = Response.Result
State = Result.NextState
print(Result.ItemId, Result.RarityId)
else
warn(Response.Error.Code, Response.Error.Message)
end
That is the entire rolling flow:
- Create an engine from rules.
- Load or create the caller-owned state.
- Pass the state into
Roll(). - Grant the returned item and save
NextState.
The example guarantees Legendary on the tenth roll without one. A natural Legendary resets that pity counter too. Soft pity can be added to the same rule when you want its weight to increase gradually before the guarantee.
An engine can track separate goals with separate counters. For example, LegendaryPity might reset on the Legendary rarity while FeaturedPetPity resets on an item tagged FeaturedPet. A single result can reset both when it matches both rules. Version 1 supports multiple reset counters and soft-pity rules, but only one rule may define hard pity.
Weights, not hard-coded percentages
Every weight is relative. RollEngine normalizes the eligible weights automatically.
With rarity weights of 75, 20, and 5, the initial chances are 75%, 20%, and 5%. Items within a rarity can use completely different ratios. Inside Common, Coins has weight 3 and Potion has weight 1, so a Common result is 75% likely to be Coins and 25% likely to be a Potion. Inside Rare, the Sword and Cape use that same 3-to-1 ratio independently of the rarity weights.
The same rules still work when pity or luck changes those weights; you do not have to manually make them total 100.
Adding a luck boost
Modifiers belong to the request, so a temporary event or player perk does not require another engine.
local DoubleLegendary = {
Id = "DoubleLegendary",
Scope = "Rarity",
Operation = "MultiplyWeight",
TargetTags = { "LuckAffected" },
Value = 2,
}
local Response = LootEngine:Roll({
State = State,
Random = RandomGenerator,
Modifiers = { DoubleLegendary },
})
Modifiers can add, multiply, or set weights, or exclude matching rarities and items. They can target IDs or tags, which lets one event modifier affect a whole tagged group without listing every entry.
Value = 2 doubles the raw Legendary weight, not its final percentage. In the 75, 20, 5 pool above, the adjusted weights become 75, 20, 10, so Legendary is 10 / 105, or about 9.52%, not 10%.
Showing the real odds
Use the current state and the same modifiers that the next roll will use:
local OddsResponse = LootEngine:GetOdds({
State = State,
Modifiers = { DoubleLegendary },
})
if OddsResponse.Success then
for _, Rarity in OddsResponse.Result.Rarities do
print(Rarity.Id, Rarity.Probability * 100 .. "%")
end
end
This is useful for an odds menu because it includes current pity, modifiers, exclusions, and normalization instead of maintaining a second approximation of the roll math. The result is exact for the supplied state and modifiers on the next roll, not a long-run observed rate.
Bonus tools: verify your roll values
The repository includes two optional development tools under RollEngine.Tools:
EmpiricalTestingcalls the realEngine:Roll()path repeatedly and returns one table containing every observed rarity rate, item rate, hard-pity count, and the final simulated state.AutoTuneuses those empirical runs to search for aMultiplyWeightmodifier that approaches a target long-run rate.
local RollEngineModule = script.RollEngine
local EmpiricalTesting = require(RollEngineModule.Tools.EmpiricalTesting)
local Response = EmpiricalTesting.Run({
Engine = LootEngine,
State = State,
Random = Random.new(12345),
Rolls = 100000,
CarryState = true,
})
if Response.Success then
local ObservedRates = {
Rarities = Response.Result.RarityRates,
Items = Response.Result.ItemRates,
HardPityCounts = Response.Result.HardPityCounts,
}
-- Format ObservedRates into one log block or display it in your own tooling.
end
CarryState = true validates the full sequential behavior, including soft pity, hard pity, natural resets, and modifiers. CarryState = false repeatedly samples one exact state. A fixed seed makes comparisons repeatable.
These tools answer different questions:
GetOdds()gives the exact probability distribution for this one next roll.EmpiricalTestinggives approximate observed rates across a sequence of real, stateful rolls.
AutoTune is deliberately development-only and approximate. Use it to suggest a value, then verify that value with a larger independent sample.
The entire core API is four operations
RollEngine.new(Rules)creates a reusable engine from a rules table.Engine:GetDefaultState()creates empty counters for those rules.Engine:Roll(Request)returns a result and the next caller-owned state.Engine:GetOdds(Request)returns the exact current rarity and item probabilities.
Roll() and GetOdds() return { Success = true, Result = ... } or { Success = false, Error = ... }, so request errors can be handled without parsing thrown error text. Invalid static rules are rejected when RollEngine.new() creates the engine.
Connecting it to a real game
A normal server flow is:
Client requests a roll
β
Server validates the request and cost
β
Server loads the player's saved RollEngine state
β
Server calls Roll()
β
Server commits the cost, reward, and NextState together
β
Server sends the result to the client for presentation
Keep the engine and the authoritative roll on the server. Do not let the client choose its own state, modifiers, random source, or reward. If a roll is monetized, also follow Robloxβs current paid-random-item rules and show players the applicable odds.
Important expectations
RollEngine uses raw weights, not fixed percentages. Roll() never mutates the supplied state, so your game must save and reuse NextState. Modifiers are applied before normalization, and natural qualifying results reset pity. GetOdds() reports the exact next-roll distribution for the supplied state and modifiers.
If you need one random system for several parts of a game, and want pity, luck, odds, and testing to agree, RollEngine provides the shared foundation without taking over the rest of your architecture.