← Back to Blog
Outdated

The Rule Engine

RobloxLuauGame Architecture

Note:
AI assisted with restructuring and grammar.

— 0MAR280

Overview

Every Roblox developer has been there.
You start with a simple shop and a couple of badges (Achievements). A few items, a few if statements checking player`s cash or level.
But as the game grows—events, bundles, time-limited items, level requirements, prerequisites—your codebase turns into a maze of nested conditional statements.
Adding a new item becomes a nightmare! And AI ends up burning through massive amounts of tokens for such a simple task.

Well, I`ve been there, too.
So, I took the time to think for a solution, once and for all!

Moving from Imperative to Declarative

Luckily, I was learning React.luau back at that time, and it had a shifting effect on my engineering mentality: instead of writing custom logic for every single product, treat rules and rewards as data.

If we take a closer look at the problem with a bit of abstraction, we'll reach a conclusion: both shop products and badges are 'evaluate-then-reward' problems.
Which means we need two separate systems—or one comprehensive system—to handle these tasks efficiently in one place, instead of repeating ourselves every time we need to add a new product.

Want to skip the implementation details? Head straight to the Conclusion & Takeaways

Engine Interface

So, how do we do that?
Well, remember those two systems?
One to evaluate, the second to reward.

I merged them into one comprehensive system: The Rule Engine!

export type RuleEngine = {
    checkAndApplyRule(player: Player, rule: RuleData): (boolean, string?)
    applyRewards(player: Player, rule: RuleData): (boolean, string?)
    checkRules(player: Player, rule: RuleData): (boolean, string?)
}

The engine utilizes a custom type RuleData.
I won't dive too much into the code, but to keep you up to speed, I'll give a brief explanation.
You can look at the documented types at src\shared\Modules\Types\Types.luau

-- A table that contains the evaluation and reward data
export type RuleData = {
    -- Where this rule must be evaluated — Server, Client or Both
	context: ContextType,
    -- A table of conditions—or a function—to evaluate.
	conditions: ({ Condition } | ((Player) -> (boolean, string?)))?,
    -- A table of rewards—or a function—to reward if the conditions passed.
	rewards: ({ Reward } | ((Player) -> (boolean, string?)))?,
}
local price = 10
 
return {
	id = 1,
	ruleData = {
		context = ENUMS.ContextTypes.Server, -- Custom Enum
		conditions = {
			{
				name = "Check Coins Balance",
				value1 = { param = BALANCES.coins }, -- User coins balance
				value2 = price, -- Sword Price
				opType = ENUMS.ConditionOpTypes.GreaterThanOrEqual,
				onFalse = `Not Enough Coins!`,
			},
		},
		rewards = {
            -- Deduct the coins
            {
                name = "Coins Payment",
		        path = dataSignal,
		        value = price,
		        opType = ENUMS.RewardOpTypes.Decrement,
		        params = {
		        	userDataPaths.coins,
		        },
            },
            -- Fire the custom signal
            {
                name = "Give Sword",
		        path = toolSignal,
		        params = {
				    "Sword", -- Will be received as "toolName"
				    "Supppp, This is an extra param!!!", -- Will be received as ...
			    },
		        unpackParams = true,
		        opType = ENUMS.RewardOpTypes.FireSignal,
            }
		},
	},
 
	attributes = {
		name = `Sword Purchase`,
		description = `Get a Sword for {coinsCost} Coins & {pointsCost} Points`,
	},
}

End-to-End Execution Flow

So, what actually happens in the background?

  • The lifecycle of a transaction:
    1. The Shop UI calls the custom MarketplaceService with the item ID.
    2. The service pulls the item data.ruleData and passes it to the RuleEngine.
    3. The engine evaluates conditions, applies rewards, and returns the results.
    4. The service updates state, fires signals, and the UI updates seamlessly.

Conclusion & Takeaways

The Code-Free Workflow

Now, adding a shop product, badges, daily logins, or quest rewards no longer requires touching core systems anymore!
Instead, it only requires creating a new module with its main data, metadata, and a ruleData property packed with clean, JSON-like tables instead of complicated conditional statements.