Skip to content
This repository has been archived by the owner on Jan 13, 2025. It is now read-only.

Commit

Permalink
Hierarchical rules
Browse files Browse the repository at this point in the history
  • Loading branch information
rcahoon committed Dec 13, 2024
1 parent af3ac74 commit 8fd414b
Show file tree
Hide file tree
Showing 4 changed files with 279 additions and 38 deletions.
72 changes: 65 additions & 7 deletions src/main/java/com/team766/framework3/Rule.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.team766.framework3;

import com.google.common.collect.Maps;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BooleanSupplier;
Expand Down Expand Up @@ -70,6 +73,8 @@ public static class Builder {
private Supplier<Procedure> onTriggeringProcedure;
private Cancellation cancellationOnFinish = Cancellation.DO_NOT_CANCEL;
private Supplier<Procedure> finishedTriggeringProcedure;
private final List<Rule.Builder> composedRules = new ArrayList<>();
private final List<Rule.Builder> negatedComposedRules = new ArrayList<>();

private Builder(String name, BooleanSupplier predicate) {
this.name = name;
Expand Down Expand Up @@ -157,14 +162,58 @@ public Builder withFinishedTriggeringProcedure(
return this;
}

/** Specify Rules which should only trigger when this Rule is also triggering. */
public Builder whenTriggering(Rule.Builder... rules) {
composedRules.addAll(Arrays.asList(rules));
return this;
}

/** Specify Rules which should only trigger when this Rule is not triggering. */
public Builder whenNotTriggering(Rule.Builder... rules) {
negatedComposedRules.addAll(Arrays.asList(rules));
return this;
}

// called by {@link RuleEngine#addRule}.
/* package */ Rule build() {
return new Rule(
name,
predicate,
onTriggeringProcedure,
cancellationOnFinish,
finishedTriggeringProcedure);
/* package */ List<Rule> build() {
return build(null);
}

private List<Rule> build(BooleanSupplier parentPredicate) {
final var rules = new ArrayList<Rule>();

final BooleanSupplier fullPredicate =
parentPredicate == null
? predicate
: () -> parentPredicate.getAsBoolean() && predicate.getAsBoolean();
final var thisRule =
new Rule(
name,
fullPredicate,
onTriggeringProcedure,
cancellationOnFinish,
finishedTriggeringProcedure);
rules.add(thisRule);

// Important! These composed predicates shouldn't invoke `predicate`. `predicate` should
// only be invoked once per call to RuleEngine.run(), so having all rules in the
// hierarchy call it would not work as expected. Instead, we have the child rules query
// the triggering state of the parent rule.
final BooleanSupplier composedPredicate =
parentPredicate == null
? () -> thisRule.isTriggering()
: () -> parentPredicate.getAsBoolean() && thisRule.isTriggering();
final BooleanSupplier negativeComposedPredicate =
parentPredicate == null
? () -> !thisRule.isTriggering()
: () -> parentPredicate.getAsBoolean() && !thisRule.isTriggering();
for (var r : composedRules) {
rules.addAll(r.build(composedPredicate));
}
for (var r : negatedComposedRules) {
rules.addAll(r.build(negativeComposedPredicate));
}
return rules;
}
}

Expand Down Expand Up @@ -232,6 +281,15 @@ public String getName() {
return currentTriggerType;
}

/* package */ boolean isTriggering() {
return switch (currentTriggerType) {
case NEWLY -> true;
case CONTINUING -> true;
case FINISHED -> false;
case NONE -> false;
};
}

/* package */ void reset() {
currentTriggerType = TriggerType.NONE;
}
Expand Down
11 changes: 6 additions & 5 deletions src/main/java/com/team766/framework3/RuleEngine.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,12 @@ public Category getLoggerCategory() {
return Category.RULES;
}

protected void addRule(Rule.Builder builder) {
Rule rule = builder.build();
rules.add(rule);
int priority = rulePriorities.size();
rulePriorities.put(rule, priority);
public void addRule(Rule.Builder builder) {
for (Rule rule : builder.build()) {
rules.add(rule);
int priority = rulePriorities.size();
rulePriorities.put(rule, priority);
}
}

@VisibleForTesting
Expand Down
166 changes: 166 additions & 0 deletions src/test/java/com/team766/framework3/RuleEngineTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -921,4 +921,170 @@ public void testRepeatedlyPersistence() {
assertNotNull(cmd2);
assertTrue(cmd2.getName().endsWith("action_ends_first_proc"));
}

/** Test hierarchical Rules triggering */
@Test
public void testRuleHierarchy() {
RuleEngine myRules =
new RuleEngine() {
{
addRule(
Rule.create("root", new ScheduledPredicate(0, 2))
.withOnTriggeringProcedure(
ONCE_AND_HOLD,
() ->
new FakeProcedure(
"root_proc", 10, Set.of(fm1)))
.whenTriggering(
Rule.create(
"positive_combinator",
new ScheduledPredicate(1, 3))
.withOnTriggeringProcedure(
ONCE_AND_HOLD,
() ->
new FakeProcedure(
"positive_combinator_proc",
10,
Set.of(fm2))))
.whenNotTriggering(
Rule.create(
"negative_combinator",
// Note: This predicate is only
// evaluated when the `root` rule is
// not triggering, so this triggers
// on frame 2, even though its
// start/end arguments say it
// triggers on frame 0.
new ScheduledPredicate(0, 1))
.withOnTriggeringProcedure(
ONCE_AND_HOLD,
() ->
new FakeProcedure(
"negative_combinator_proc",
10,
Set.of(fm3)))));
}
};

myRules.run();

Command cmd1 = CommandScheduler.getInstance().requiring(fm1);
assertNotNull(cmd1);
assertTrue(cmd1.getName().endsWith("root_proc"));
Command cmd2 = CommandScheduler.getInstance().requiring(fm2);
assertNull(cmd2);
Command cmd3 = CommandScheduler.getInstance().requiring(fm3);
assertNull(cmd3);

step();
myRules.run();

cmd1 = CommandScheduler.getInstance().requiring(fm1);
assertNotNull(cmd1);
assertTrue(cmd1.getName().endsWith("root_proc"));
cmd2 = CommandScheduler.getInstance().requiring(fm2);
assertNotNull(cmd2);
assertTrue(cmd2.getName().endsWith("positive_combinator_proc"));
cmd3 = CommandScheduler.getInstance().requiring(fm3);
assertNull(cmd3);

step();
myRules.run();

cmd1 = CommandScheduler.getInstance().requiring(fm1);
assertNull(cmd1);
cmd2 = CommandScheduler.getInstance().requiring(fm2);
assertNull(cmd2);
cmd3 = CommandScheduler.getInstance().requiring(fm3);
assertNotNull(cmd3);
assertTrue(cmd3.getName().endsWith("negative_combinator_proc"));

step();
myRules.run();

cmd1 = CommandScheduler.getInstance().requiring(fm1);
assertNull(cmd1);
cmd2 = CommandScheduler.getInstance().requiring(fm2);
assertNull(cmd2);
cmd3 = CommandScheduler.getInstance().requiring(fm3);
assertNull(cmd3);
}

/** Test that the root Rule takes precedence over child rules triggering */
@Test
public void testRuleHierarchyPriorities() {
RuleEngine myRules =
new RuleEngine() {
{
addRule(
Rule.create("root", new ScheduledPredicate(0, 2))
.withOnTriggeringProcedure(
ONCE,
() ->
new FakeProcedure(
"root_newly_proc", 0, Set.of(fm1)))
.withFinishedTriggeringProcedure(
() ->
new FakeProcedure(
"root_finished_proc",
0,
Set.of(fm1)))
.whenTriggering(
Rule.create(
"positive_combinator",
new ScheduledPredicate(0, 2))
.withOnTriggeringProcedure(
ONCE_AND_HOLD,
() ->
new FakeProcedure(
"positive_combinator_proc",
10,
Set.of(fm1))))
.whenNotTriggering(
Rule.create(
"negative_combinator",
// Note: This predicate is only
// evaluated when the `root` rule is
// not triggering, so this triggers
// on frames 2-3, even though its
// start/end arguments say it
// triggers on frame 0-1.
new ScheduledPredicate(0, 2))
.withOnTriggeringProcedure(
ONCE_AND_HOLD,
() ->
new FakeProcedure(
"negative_combinator_proc",
10,
Set.of(fm1)))));
}
};

myRules.run();

Command cmd1 = CommandScheduler.getInstance().requiring(fm1);
assertNotNull(cmd1);
assertTrue(cmd1.getName().endsWith("root_newly_proc"));

step();
myRules.run();

cmd1 = CommandScheduler.getInstance().requiring(fm1);
assertNotNull(cmd1);
assertTrue(cmd1.getName().endsWith("positive_combinator_proc"));

step();
myRules.run();

cmd1 = CommandScheduler.getInstance().requiring(fm1);
assertNotNull(cmd1);
assertTrue(cmd1.getName().endsWith("root_finished_proc"));

step();
myRules.run();

cmd1 = CommandScheduler.getInstance().requiring(fm1);
assertNotNull(cmd1);
assertTrue(cmd1.getName().endsWith("negative_combinator_proc"));
}
}
Loading

0 comments on commit 8fd414b

Please sign in to comment.