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

Hierarchical rules #147

Open
wants to merge 1 commit into
base: rcahoon/mf3-rule-persistence
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
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 @@ -231,6 +280,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
Loading