Kissrule
This is a lightweight rule-based system for game AI micro-decisions, written in C++20. It lets you register object hierarchies as fact sources, load rule sets from files, and evaluate them at runtime with lazy fact fetching and caching.
Copyright © 2026 Laurent Couvidou
Contact: hello@lorancou.net
Source: https://codeberg.org/lorancou/kissrule
Library features
- Rules defined as flexible lists of criteria evaluating facts.
- Fully serializable to JSON and TOML.
- Rich comparison operators for criteria.
- Flexible fact source definitions.
- Enum support with typed enum values in rules.
- Multiple rule ordering policies.
- Multiple response execution policies with runtime filtering.
- Performant runtime evaluation.
Usage
// 1. Declare your C++ types.
// Let's say WeaponType, Weapon, Attacker are your own game types.
Registry registry;
registry.register_enum<WeaponType>(
"weapon_type"_sym,
{{"sword"_sym, WeaponType::sword},
{"gun"_sym, WeaponType::gun}});
registry.register_fact_source<Weapon>(
"weapon"_sym,
{{"type"_sym, &Weapon::get_type},
{"ammo"_sym, &Weapon::get_ammo}});
registry.register_fact_source<Attacker>(
"attacker"_sym,
{{"health"_sym, &Attacker::get_health}}, // value facts
{{"weapon"_sym, &Attacker::get_weapon}}); // child fact sources
// 2. Define a rule spec: a type of rule matching a use case.
// Here for example, let's define rules for an NPC to attack.
registry.register_rule_spec(
"attack_spec"_sym,
{{"attacker"_sym, "self"_sym}});
// 3. Define a rule set. Built in C++ here, but this is what loads from JSON/TOML.
// Here for example: "If health is low and the gun still has ammo, retreat and shoot."
const RuleSet rules = {
"attack_spec"_sym,
"retreat_and_shoot"_sym,
{{{{"self.health"_sym_p, LessThan{0.3f}},
{"self.weapon.type"_sym_p, EqualTo{WeaponType::gun}},
{"self.weapon.ammo"_sym_p, GreaterThan{0}}},
{{"retreat"_sym}, {"shoot"_sym}}}}};
// 4. Bind runtime fact sources and response executors.
Attacker self;
RuleContext context(registry);
context.bind_fact_source(self, "self"_sym);
context.response_executors["retreat"_sym] = [] { /* run retreat logic */ };
context.response_executors["shoot"_sym] = [] { /* run shoot logic */ };
// 5. Compile once -- this is costly.
RuleEvaluator evaluator(context, rules);
// 6. Evaluate often, e.g. every frame. This is cheap!
evaluator.evaluate();
Coding convention
We're loosely following the LLVM coding standards: https://llvm.org/docs/CodingStandards.html, with these naming rules: CamelCase for types (structs, classes, enums, type aliases) and template parameters (full words, e.g. Type, not T), snake_case for everything else (functions, variables, enum values, namespaces, file names). Private member variables end with an underscore: member_variable_; public member variables don't. For std types we use CamelCase aliases (String, Vector, UniquePtr, ...), in hope to make data structures more easy to the eye.
Known limitations
- Rule compilation is heavyweight and probably has room for improvement on the performance side.
- For optimization reasons, 64-bit value types only allow same-type equality, i.e. ordinal comparisons are not supported.
- We're only joining criteria with AND operators. We may someday consider allowing OR operators in edited/serialized rules, and flattening them for runtime.
Third-party dependencies
All vendored libraries live under third_party/. Each directory contains a license file.
| Library | Purpose | License |
|---|---|---|
| doctest | Test framework | MIT |
| nlohmann/json | JSON parsing | MIT |
| tomlplusplus | TOML parsing | MIT |
| wyhash | String hashing + PRNG | Unlicense (public domain) |
Future plans
- Consider integrating https://include-what-you-use.org/
- Standalone rules editor (Qt6 DLLs + vcpkg, or maybe C#).
- Godot integration (GDExtension, should be fine with C++20, even if the engine itself is C++17 -- at least according to Claude).
- Unreal integration (a plugin, most likely).
License
Kissrule is distributed under the Mozilla Public License 2.0. See LICENSE.txt.