Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MIM-1: Trait-based rulesets #29

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions crates/subtale-mimir/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,30 @@ version = "0.5.1"
edition = "2021"
authors = ["Luke Carr <[email protected]>"]
description = "Contextual query engine for dynamic video games"
homepage = "https://mimir.subtale.com"
homepage = "https://mimir.subtale.dev"
repository = "https://github.com/subtalegames/mimir"
license = "MIT OR Apache-2.0"
readme = "README.md"

[dependencies]
float-cmp = { version = "0.9", optional = true }
indexmap = "2.2"
rand = "0.8"
serde = { version = "1.0", features = ["derive"], optional = true }

[dev-dependencies]
criterion = "0.5"
rand = "0.8"

[[bench]]
name = "float_evaluator"
harness = false

[[bench]]
name = "ruleset_evaluation"
name = "weighted_ruleset_evaluation"
harness = false

[[bench]]
name = "ruleset_init"
name = "weighted_ruleset_init"
harness = false

[features]
Expand Down
14 changes: 7 additions & 7 deletions crates/subtale-mimir/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ Your game's world is defined as a collection of facts: the player killed x amoun

In Mímir, facts are collected together into a map ([`Query<FactKey, FactType>`][query]), where the key is the unique identifier of the fact, and the value is the fact's value.

Also, your game will (most likey!) have predefined rules that define behaviour that should occur when one or more facts are true. We represent rules as a map ([`Rule<FactKey, FactType, FactEvaluator, Outcome>`][rule]), where the key is the unique identifier of the fact, and the value is a predicate ([`Evaluator`][evaluator]) that is evaluated against the fact's value.
Also, your game will (most likely!) have predefined rules that define behaviour that should occur when one or more facts are true. We represent rules as a map ([`Rule<FactKey, FactType, FactEvaluator, Outcome>`][rule]), where the key is the unique identifier of the fact, and the value is a predicate ([`Evaluator`][evaluator]) that is evaluated against the fact's value.

Finally, rules can be stored together in collections known as rulesets ([`Ruleset<FactKey, FactType, FactEvaluator, Outcome>`][ruleset]). Rulesets allow a query to be evaluated against many rules at once: Mímir will always look to match a query against the rule in the ruleset with the most requirements (i.e. more specific). *(If multiple rules are matched with the same specificity, one is chosen at random.)*
Finally, rules can be stored together in collections known as rulesets ([`Ruleset<FactKey, FactType, FactEvaluator, Outcome>`][ruleset]). Rulesets allow a query to be evaluated against many rules at once: depending on the ruleset implementation, the query will return either the first rule that is satisfied (see `WeightedRuleset`), or all rules that are satisfied (see `SimpleRuleset`).

## Example

Expand All @@ -40,15 +40,15 @@ more_specific_rule.insert("enemies_killed", FloatEvaluator::EqualTo(5.));
more_specific_rule.insert("doors_opened", FloatEvaluator::gte(2.));

// bundle the rules into a ruleset
let ruleset = Ruleset::new(vec![rule, more_specific_rule]);
let ruleset = WeightedRuleset::new(vec![rule, more_specific_rule]);

// run a query against the ruleset
let mut query = Query::new();
// Query<&str, f64>
query.insert("enemies_killed", 2.5 + 1.5 + 1.);

assert_eq!(
ruleset.evaluate(&query).unwrap().outcome,
ruleset.evaluate(&query).first().unwrap().outcome,
"You killed 5 enemies!"
);

Expand All @@ -58,14 +58,14 @@ more_specific_query.insert("enemies_killed", 2.5 + 1.5 + 1.);
more_specific_query.insert("doors_opened", 10.);

assert_eq!(
ruleset.evaluate(&more_specific_query).unwrap().outcome,
ruleset.evaluate(&more_specific_query).first().unwrap().outcome,
"You killed 5 enemies and opened 2 doors!"
);
```

In the above example, we define a ruleset with two rules. Both rules require that 5 enemies have been killed, but one rule is more specific (also requiring that more than 2 doors have been opened).
In the above example, we define a weighted ruleset with two rules. Both rules require that 5 enemies have been killed, but one rule has a higher weight because it' more specific (also requiring that more than 2 doors have been opened).

The first query evaluates to the simpler rule, because the query does not satisfy the doors opened requirement. However, the second query evaluates to the more complex rule because the query *does* satistfy the doors opened requirement (note that even though the simpler rule is still satisfied, Mímir does not evaluate it as true because it's less specific/contains fewer requirements).
The first query evaluates to the simpler rule, because the query does not satisfy the doors opened requirement. However, the second query evaluates to the more complex rule because the query *does* satisfy the doors opened requirement (note that even though the lesser weighted rule is still satisfied, Mímir does not evaluate it as true because we're using a `WeightedRuleset`).

[docs]: https://mimir.subtale.com
[tutorial]: https://mimir.subtale.com/tutorial
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ fn benchmark(c: &mut Criterion) {
rule_2.insert("fact_2", FloatEvaluator::lt(6.0));
rule_2.insert("fact_3", FloatEvaluator::range(9.0, 12.0));

let ruleset = Ruleset::new(vec![rule_1, rule_2]);
let ruleset = WeightedRuleset::new(vec![rule_1, rule_2]);

c.bench_function("ruleset evaluate", |b| b.iter(|| ruleset.evaluate(&query)));
c.bench_function("weighted ruleset evaluate", |b| {
b.iter(|| ruleset.evaluate(&query))
});
}

#[cfg(feature = "float")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use subtale_mimir::prelude::*;
fn benchmark(c: &mut Criterion) {
let mut rng = rand::thread_rng();

let mut group = c.benchmark_group("ruleset init");
let mut group = c.benchmark_group("weighted ruleset init");

for &num_rules in &[10, 100, 1_000, 10_000] {
group.bench_function(format!("{} rules", num_rules), |b| {
Expand All @@ -20,7 +20,7 @@ fn benchmark(c: &mut Criterion) {
})
.collect();

let _ruleset = Ruleset::new(rules);
let _ruleset = WeightedRuleset::new(rules);
});
});
}
Expand Down
4 changes: 1 addition & 3 deletions crates/subtale-mimir/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,7 @@ where
pub facts: IndexMap<FactKey, FactType>,
}

impl<FactKey: std::hash::Hash + Eq, FactType: Copy>
Query<FactKey, FactType>
{
impl<FactKey: std::hash::Hash + Eq, FactType: Copy> Query<FactKey, FactType> {
/// Instantiates a new instance of `Query` without allocating an underlying
/// `IndexMap`.
///
Expand Down
Loading