Spell & Cheat-Code Mini Expression Language
Hardcoding quest conditions, combat formulas, and loot rules directly in C++ forces an engine recompile and binary patch for every balance tweak. Conversely, naive ad-hoc string parsing in the game loop fails on operator precedence, crashes on nested parentheses, and causes severe allocation churn inside 60 FPS combat.
Evaluating BASE + 3 * INT strictly left-to-right computes (BASE + 3) * INT (giving 230 instead of 50 for BASE=20, INT=10), drastically distorting combat balance.
Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.
We represent spell formulas as an Abstract Syntax Tree (AST) rooted at the Expr interface. Terminal expressions (NumberLiteral, StatVar) form leaf nodes that evaluate directly without recursion. Non-terminal expressions (AddExpr, MultiplyExpr) compose child expressions and evaluate binary operations. A FormulaParser compiles formula text into an AST once. During combat, casting a spell invokes ast->eval(statContext), evaluating tree nodes in nanoseconds.
Across Sprints 1 through 7, we engineered our complete RPG Game Engine: stackable buff decorators (C1), nested composite inventories (C2), encapsulated memento checkpoints (C3), cloned prototype monster hordes (C4), memory-budgeted flyweight biomes (C5), decoupled bridge graphics backends (C6), and non-invasive visitor analytics passes (C7). Now we face our final engine milestone: data-driven gameplay scripting.
We need an expression language engine where combat designers and quest writers can write data-driven rules ("level >= 10 AND HAS_ITEM DragonKey") and dynamic spell damage formulas (BASE + 3 * INT, ( 10 + LEVEL ) * 2) in external config files, evaluated dynamically against runtime character attributes (StatContext).
Hardcoding quest conditions, combat formulas, and loot rules directly in C++ forces an engine recompile and binary patch for every balance tweak. Conversely, naive ad-hoc string parsing in the game loop fails on operator precedence, crashes on nested parentheses, and causes severe allocation churn inside 60 FPS combat.
The Interpreter Pattern parses formula and rule strings into an Abstract Syntax Tree (AST) of terminal (NumberLiteral, StatVar) and non-terminal (AddExpr, MultiplyExpr) nodes evaluated in nanoseconds against player state without recompilation. This completes our production-grade RPG Game Engine architecture!
The product ticket
Game designers need to write custom damage formulas for spells, buff scaling, and cheat codes without requesting engineering deployments for every formula balance tweak. Spells like Fireball calculate BASE + 3 * INT, while Frost Nova uses ( 10 + LEVEL ) * 2, evaluated dynamically against a hero's current StatContext (LEVEL, INT, STR, BASE).
- β’
Parse and evaluate mathematical formulas composed of integer constants, character stat identifiers (
LEVEL,INT,STR,BASE), addition (+), and multiplication (*). - β’
Correctly observe standard operator precedence (
*before+) and support nested parentheses( ... ). - β’
Evaluate compiled expressions repeatedly against varying character
StatContextsnapshots with zero per-cast re-parsing overhead. - β’
Support graceful error handling and default fallbacks for undefined stats.
Next sprint, combat design introduces item modifier formulas with subtraction (-) and division (/). The naive ad-hoc string scanner breaks completely on operator precedence and crashes whenever parentheses appear.
Step 1 β The naive code & why it breaks
The naive implementation splits formula strings with std::istringstream and computes values strictly left-to-right on every spell cast, ignoring arithmetic precedence and failing on parentheses.
#include <cctype>
#include <print>
#include <sstream>
#include <string>
#include <unordered_map>
#include <vector>
// Naive Spell Evaluator: parses string tokens strictly left-to-right without an AST.
// Completely fails operator precedence (evaluates 10 + 2 * 3 as (10+2)*3 = 36 instead of 16),
// cannot handle nested parentheses, and re-parses strings on every single spell cast.
class NaiveSpellEvaluator {
public:
static int evaluate(const std::string& formula, const std::unordered_map<std::string, int>& stats) {
std::istringstream stream(formula);
std::string token;
int currentVal = 0;
char lastOp = '+';
bool isFirst = true;
while (stream >> token) {
if (token == "+" || token == "*") {
lastOp = token[0];
} else {
int operand = 0;
if (std::isdigit(static_cast<unsigned char>(token[0]))) {
operand = std::stoi(token);
} else {
auto it = stats.find(token);
operand = (it != stats.end()) ? it->second : 0;
}
if (isFirst) {
currentVal = operand;
isFirst = false;
} else if (lastOp == '+') {
currentVal += operand;
} else if (lastOp == '*') {
// Flawed left-to-right: ignores precedence!
currentVal *= operand;
}
}
}
return currentVal;
}
};
int main() {
std::unordered_map<std::string, int> heroStats{
{"BASE", 20},
{"INT", 10},
{"LEVEL", 5}
};
// 1. Formula with precedence: BASE + 3 * INT
// Expected math: 20 + (3 * 10) = 50.
// Naive left-to-right: (20 + 3) * 10 = 230! (WRONG)
std::string f1 = "BASE + 3 * INT";
int res1 = NaiveSpellEvaluator::evaluate(f1, heroStats);
std::println("Formula: '{}' -> Naive Result: {} (Expected: 50)", f1, res1);
// 2. Parentheses are completely unsupported and will parse as 0 or crash
std::string f2 = "( 10 + LEVEL ) * 2";
int res2 = NaiveSpellEvaluator::evaluate(f2, heroStats);
std::println("Formula with parentheses: '{}' -> Naive Result: {} (Broken!)", f2, res2);
return 0;
}Evaluating BASE + 3 * INT strictly left-to-right computes (BASE + 3) * INT (giving 230 instead of 50 for BASE=20, INT=10), drastically distorting combat balance.
Parentheses like ( 10 + LEVEL ) * 2 cannot be evaluated with a simple sequential accumulator without an Abstract Syntax Tree (AST) or operator stack.
Splitting and scanning formula strings on every cast inside a 60 FPS multiplayer combat loop produces hundreds of unnecessary heap allocations per second.
Step 2 β The fix: Interpreter Pattern
Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.
We represent spell formulas as an Abstract Syntax Tree (AST) rooted at the Expr interface. Terminal expressions (NumberLiteral, StatVar) form leaf nodes that evaluate directly without recursion. Non-terminal expressions (AddExpr, MultiplyExpr) compose child expressions and evaluate binary operations. A FormulaParser compiles formula text into an AST once. During combat, casting a spell invokes ast->eval(statContext), evaluating tree nodes in nanoseconds.
Expr β declares pure virtual eval(const StatContext& ctx) const = 0; and a debugging repr() string method.
NumberLiteral and StatVar β evaluate leaf values (integer constants and variable lookups) without recursing.
AddExpr and MultiplyExpr β composite expressions that own left_ and right_ child expressions and combine their results.
StatContext β maintains key-value mappings of runtime character attributes (BASE, INT, LEVEL, STR).
SpellFormula and FormulaParser β tokenize formula strings into the composite AST structure once at initialization.
Unlock Full UML Diagram & C++23 Solution
The interactive UML architecture diagrams, refactored C++23 code, real-world extensions, and interview Q&A are part of Brute2Best Pro. All 7 foundation chapters are 100% free.
First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.