From ee5988a34d8b52d35fe6999e65942cde8ddc083e Mon Sep 17 00:00:00 2001 From: Jens Steenmetz <35835627+JensSteenmetz@users.noreply.github.com> Date: Sun, 23 Jun 2024 14:34:55 +0200 Subject: [PATCH] feat!: simplify `Goal` (#71) * feat!: simplify Goal * test: fix Goal tests --- Aplib.Core.Tests/Desire/GoalTests.cs | 108 ++++------ Aplib.Core.Tests/Tools/TestGoalBuilder.cs | 39 ---- .../Desire/Goals/CommonHeuristicFunctions.cs | 42 ---- Aplib.Core/Desire/Goals/Goal.cs | 191 ++++++------------ Aplib.Core/Desire/Goals/Heuristics.cs | 22 -- Aplib.Core/Desire/Goals/IGoal.cs | 6 - 6 files changed, 96 insertions(+), 312 deletions(-) delete mode 100644 Aplib.Core.Tests/Tools/TestGoalBuilder.cs delete mode 100644 Aplib.Core/Desire/Goals/CommonHeuristicFunctions.cs delete mode 100644 Aplib.Core/Desire/Goals/Heuristics.cs diff --git a/Aplib.Core.Tests/Desire/GoalTests.cs b/Aplib.Core.Tests/Desire/GoalTests.cs index 3cfa7ce7..4be5fe06 100644 --- a/Aplib.Core.Tests/Desire/GoalTests.cs +++ b/Aplib.Core.Tests/Desire/GoalTests.cs @@ -2,7 +2,6 @@ using Aplib.Core.Desire.Goals; using Aplib.Core.Intent.Actions; using Aplib.Core.Intent.Tactics; -using Aplib.Core.Tests.Tools; using FluentAssertions; using Moq; using System.Diagnostics.CodeAnalysis; @@ -21,7 +20,6 @@ public void Goal_WhenConstructed_ContainsCorrectMetaData() { // Arrange ITactic tactic = Mock.Of>(); - Goal.HeuristicFunction heuristicFunction = CommonHeuristicFunctions.Constant(0f); const string name = "Such a good goal name"; const string description = "\"A lie is just a good story that someone ruined with the truth.\" - Barney Stinson"; @@ -29,7 +27,7 @@ public void Goal_WhenConstructed_ContainsCorrectMetaData() // Act // Does not use helper methods on purpose - Goal goal = new(metadata, tactic, heuristicFunction: heuristicFunction); + Goal goal = new(metadata, tactic, _ => false); // Assert goal.Should().NotBeNull(); @@ -50,7 +48,7 @@ public void Goal_WhenConstructed_DidNotIterateYet() tactic.Setup(x => x.GetAction(It.IsAny())).Returns(new Action(_ => { iterations++; })); // Act - Goal goal = new TestGoalBuilder().UseTactic(tactic.Object).Build(); + Goal goal = new(tactic.Object, _ => false); // Assert goal.Tactic.Should().Be(tactic.Object); @@ -58,19 +56,20 @@ public void Goal_WhenConstructed_DidNotIterateYet() } /// - /// Given the Goal's heuristic function is configured to *not* have reached its goal, - /// when the Evaluate() method of a goal is used, - /// then the method should return false. + /// Given the Goal's predicate evaluates to false, + /// when the UpdateStatus() method of a goal is used, + /// then Status should return false. /// [Fact] public void Goal_WhenNotReached_DoesNotReturnAsCompleted() { // Arrange - Goal.HeuristicFunction heuristicFunction = CommonHeuristicFunctions.Uncompleted(); + IBeliefSet beliefSet = Mock.Of(); + ITactic tactic = Mock.Of>(); + Goal goal = new(tactic, _ => false); // Act - Goal goal = new TestGoalBuilder().WithHeuristicFunction(heuristicFunction).Build(); - goal.UpdateStatus(It.IsAny()); + goal.UpdateStatus(beliefSet); CompletionStatus isCompleted = goal.Status; // Assert @@ -78,19 +77,20 @@ public void Goal_WhenNotReached_DoesNotReturnAsCompleted() } /// - /// Given the Goal's heuristic function is configured to have reached its goal - /// when the Evaluate() method of a goal is used, + /// Given the Goal's predicate evaluates to true, + /// when the UpdateStatus() method of a goal is used, /// then the method should return true. /// [Fact] public void Goal_WhenReached_ReturnsAsCompleted() { // Arrange - Goal.HeuristicFunction heuristicFunction = CommonHeuristicFunctions.Completed(); + IBeliefSet beliefSet = Mock.Of(); + ITactic tactic = Mock.Of>(); + Goal goal = new(tactic, _ => true); // Act - Goal goal = new TestGoalBuilder().WithHeuristicFunction(heuristicFunction).Build(); - goal.UpdateStatus(It.IsAny()); + goal.UpdateStatus(beliefSet); CompletionStatus isCompleted = goal.Status; // Assert @@ -99,7 +99,7 @@ public void Goal_WhenReached_ReturnsAsCompleted() /// /// Given a valid goal and belief, - /// when the goal's heuristic function is evaluated, + /// when the goal's predicate is evaluated, /// the belief set is not altered /// [Fact] @@ -107,34 +107,35 @@ public void Goal_WhereEvaluationIsPerformed_DoesNotInfluenceBelieveSet() { // Arrange Mock beliefSetMock = new(); - Goal goal = new TestGoalBuilder().Build(); + ITactic tactic = Mock.Of>(); + Goal goal = new(tactic, _ => false); // Act - goal.UpdateStatus(It.IsAny()); + goal.UpdateStatus(beliefSetMock.Object); // Assert beliefSetMock.Verify(beliefSet => beliefSet.UpdateBeliefs(), Times.Never); } /// - /// Given a valid goal with heuristics - /// when the goal's heuristic function's result will be different in the next frame - /// the most recent heuristics are used + /// Given a valid goal + /// when the goal's predicate result will be different in the next frame + /// the most recent result is used /// [Fact] public void Goal_WhereHeuristicsChange_UsesUpdatedHeuristics() { // Arrange - IBeliefSet beliefSetMock = Mock.Of(); + IBeliefSet beliefSet = Mock.Of(); + ITactic tactic = Mock.Of>(); bool shouldSucceed = false; - // ReSharper disable once AccessToModifiedClosure - Goal goal = new TestGoalBuilder().WithHeuristicFunction(_ => shouldSucceed).Build(); + Goal goal = new(tactic, _ => shouldSucceed); // Act - goal.UpdateStatus(beliefSetMock); + goal.UpdateStatus(beliefSet); CompletionStatus stateBefore = goal.Status; shouldSucceed = true; // Make heuristic function return a different value on next invocation. - goal.UpdateStatus(beliefSetMock); + goal.UpdateStatus(beliefSet); CompletionStatus stateAfter = goal.Status; // Assert @@ -142,64 +143,31 @@ public void Goal_WhereHeuristicsChange_UsesUpdatedHeuristics() stateAfter.Should().Be(CompletionStatus.Success); } - /// - /// Given the Goal's different constructors have been called with semantically equal arguments - /// when the Evaluate() method of all goals are used, - /// then all returned values should equal. - /// - /// - [Theory] - [InlineData(true)] - [InlineData(false)] - public void GoalConstructor_WhereHeuristicFunctionTypeDiffers_HasEqualBehaviour(bool goalCompleted) - { - // Arrange - ITactic tactic = Mock.Of>(); - - Goal.HeuristicFunction heuristicFunctionNonBoolean = - CommonHeuristicFunctions.Boolean(_ => goalCompleted); - - Goal goalBoolean = new(tactic, HeuristicFunctionBoolean); - Goal goalNonBoolean = new(tactic, heuristicFunctionNonBoolean); - - // Act - goalBoolean.UpdateStatus(It.IsAny()); - CompletionStatus goalBooleanEvaluation = goalBoolean.Status; - goalNonBoolean.UpdateStatus(It.IsAny()); - CompletionStatus goalNonBooleanEvaluation = goalNonBoolean.Status; - - // Assert - goalBooleanEvaluation.Should().Be(goalNonBooleanEvaluation); - return; - - bool HeuristicFunctionBoolean(IBeliefSet _) => goalCompleted; - } - [Fact] public void UpdateStatus_WhenFailGuardIsTrue_FailsTheGoal() { // Arrange + IBeliefSet beliefSet = Mock.Of(); ITactic tactic = Mock.Of>(); - Goal.HeuristicFunction heuristic = CommonHeuristicFunctions.Boolean(_ => false); - Goal goal = new(tactic, heuristic, _ => true); + Goal goal = new(tactic, predicate: _ => false, failGuard: _ => true); // Act - goal.UpdateStatus(It.IsAny()); + goal.UpdateStatus(beliefSet); // Assert goal.Status.Should().Be(CompletionStatus.Failure); } [Fact] - public void UpdateStatus_WhenBothFailGuardIsTrueAndHeuristicIsReached_CompletesTheGoal() + public void UpdateStatus_WhenBothFailGuardIsTrueAndPredicateIsTrue_CompletesTheGoal() { // Arrange + IBeliefSet beliefSet = Mock.Of(); ITactic tactic = Mock.Of>(); - Goal.HeuristicFunction heuristic = CommonHeuristicFunctions.Boolean(_ => true); - Goal goal = new(tactic, heuristic, _ => true); + Goal goal = new(tactic, predicate: _ => true, failGuard: _ => true); // Act - goal.UpdateStatus(It.IsAny()); + goal.UpdateStatus(beliefSet); // Assert goal.Status.Should().Be(CompletionStatus.Success); @@ -208,15 +176,15 @@ public void UpdateStatus_WhenBothFailGuardIsTrueAndHeuristicIsReached_CompletesT [Theory] [InlineData(true)] [InlineData(false)] - public void Goal_WithoutFailGuard_DoesNotFail(bool goalCompleted) + public void Goal_WithoutFailGuard_DoesNotFail(bool shouldSucceed) { // Arrange + IBeliefSet beliefSet = Mock.Of(); ITactic tactic = Mock.Of>(); - Goal.HeuristicFunction heuristic = CommonHeuristicFunctions.Boolean(_ => goalCompleted); + Goal goal = new(tactic, predicate: _ => shouldSucceed); // Act - Goal goal = new(tactic, heuristic); - goal.UpdateStatus(It.IsAny()); + goal.UpdateStatus(beliefSet); // Assert goal.Status.Should().NotBe(CompletionStatus.Failure); diff --git a/Aplib.Core.Tests/Tools/TestGoalBuilder.cs b/Aplib.Core.Tests/Tools/TestGoalBuilder.cs deleted file mode 100644 index 1b720ba8..00000000 --- a/Aplib.Core.Tests/Tools/TestGoalBuilder.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Aplib.Core.Belief.BeliefSets; -using Aplib.Core.Desire.Goals; -using Aplib.Core.Intent.Tactics; -using Moq; - -namespace Aplib.Core.Tests.Tools; - -internal sealed class TestGoalBuilder -{ - private string _description = "\"A lie is just a good story that someone ruined with the truth.\" ~ Barney Stinson"; - private Goal.HeuristicFunction _heuristicFunction = CommonHeuristicFunctions.Constant(0); - private string _name = "Such a good goal name"; - private ITactic _tactic = Mock.Of>(); - - public TestGoalBuilder WithHeuristicFunction(Goal.HeuristicFunction heuristicFunction) - { - _heuristicFunction = heuristicFunction; - return this; - } - - public TestGoalBuilder WithHeuristicFunction(System.Predicate heuristicFunction) - => WithHeuristicFunction(CommonHeuristicFunctions.Boolean(heuristicFunction)); - - public TestGoalBuilder UseTactic(ITactic tactic) - { - _tactic = tactic; - return this; - } - - public TestGoalBuilder WithMetaData(string name, string description) - { - _name = name; - _description = description; - return this; - } - - - public Goal Build() => new(new Metadata(_name, _description), _tactic, _heuristicFunction); -} diff --git a/Aplib.Core/Desire/Goals/CommonHeuristicFunctions.cs b/Aplib.Core/Desire/Goals/CommonHeuristicFunctions.cs deleted file mode 100644 index 83f3c39d..00000000 --- a/Aplib.Core/Desire/Goals/CommonHeuristicFunctions.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Aplib.Core.Belief.BeliefSets; - -namespace Aplib.Core.Desire.Goals -{ - /// - /// Contains helper methods to generate commonly used heuristic functions. - /// - public static class CommonHeuristicFunctions - where TBeliefSet : IBeliefSet - { - /// - /// Converts a boolean-based heuristic function to a . - /// - /// - /// A heuristic function which returns true only when the state is considered completed. - /// - /// A heuristic function which wraps around the boolean-based heuristic function. - public static Goal.HeuristicFunction Boolean(System.Predicate heuristicFunction) - => beliefSet => Heuristics.Boolean(heuristicFunction.Invoke(beliefSet)); - - /// - /// A which always returns with the same distance. - /// - /// The distance which the heuristic function must always return. - public static Goal.HeuristicFunction Constant(float distance) - => _ => new Heuristics { Distance = distance }; - - /// - /// Returns a heuristic function which always, at all times, and forever, returns a value indicating the state - /// can be seen as completed. - /// - /// Said heuristic function. - public static Goal.HeuristicFunction Completed() => Constant(0f); - - /// - /// Returns a heuristic function which always, at all times, and forever, returns a value indicating the state - /// can be seen as NOT completed. - /// - /// Said heuristic function. - public static Goal.HeuristicFunction Uncompleted() => Constant(float.PositiveInfinity); - } -} diff --git a/Aplib.Core/Desire/Goals/Goal.cs b/Aplib.Core/Desire/Goals/Goal.cs index 4069594c..b145d1a9 100644 --- a/Aplib.Core/Desire/Goals/Goal.cs +++ b/Aplib.Core/Desire/Goals/Goal.cs @@ -4,6 +4,7 @@ using Aplib.Core.Logging; using System.Collections.Generic; using System.Linq; +using static Aplib.Core.CompletionStatus; namespace Aplib.Core.Desire.Goals { @@ -18,44 +19,23 @@ public class Goal : IGoal, ILoggable where TBeliefSet : IBeliefSet { /// - /// The default value for the epsilon parameter in the Goal constructors. - /// The epsilon parameter defines the threshold distance for a goal to be considered completed. + /// A predicate that determines whether the goal has succeeded. + /// Intuitively, the predicate is the goal itself. /// - protected const double DefaultEpsilon = 0.005d; + protected internal readonly System.Predicate _predicate; /// - /// The goal is considered to be completed, when the distance of the is below - /// this value. - /// - protected readonly double _epsilon; - - /// - /// A fail-guard for the goal's completion status. + /// An (optional) fail-guard for the goal's completion status. /// The fail-guard predicate is a condition that, when true, indicates that the goal has failed. /// - protected readonly System.Predicate _failGuard; - - /// - /// The concrete implementation of this Goal's . Used to test whether this goal is - /// completed. - /// - /// - protected readonly HeuristicFunction _heuristicFunction; - - /// - /// The abstract definition of what is means to test the Goal's heuristic function. Returns , as - /// they represent how close we are to matching the heuristic function, and if the goal is completed. - /// - /// - public delegate Heuristics HeuristicFunction(TBeliefSet beliefSet); + protected internal readonly System.Predicate _failGuard; /// public IMetadata Metadata { get; } /// - /// The used to achieve this , which is - /// executed during every - /// iteration of the BDI cycle. + /// The used to achieve this . + /// It is executed once in every iteration of the BDI cycle while this goal is the active goal of the agent. /// public ITactic Tactic { get; } @@ -66,74 +46,6 @@ public class Goal : IGoal, ILoggable /// public CompletionStatus Status { get; protected set; } - /// - /// Initializes a new goal from a given tactic and heuristic function, and an optional fail-guard. - /// - /// - /// Metadata about this goal, used to quickly display the goal in several contexts. - /// If omitted, default metadata will be generated. - /// - /// The tactic used to approach this goal. - /// The heuristic function which defines whether a goal is reached. - /// - /// A predicate that determines when the goal has failed. - /// If the fail-guard is true, - /// but the success heuristic is also satisfied, the success heuristic takes precedence. - /// If omitted, the goal will never fail. - /// - /// - /// The goal is considered to be completed when the result of the heuristic function is below this value. - /// - public Goal - ( - IMetadata metadata, - ITactic tactic, - HeuristicFunction heuristicFunction, - System.Predicate failGuard, - double epsilon = DefaultEpsilon - ) - { - Metadata = metadata; - Tactic = tactic; - _heuristicFunction = heuristicFunction; - _failGuard = failGuard; - _epsilon = epsilon; - } - - /// - public Goal - ( - ITactic tactic, - HeuristicFunction heuristicFunction, - System.Predicate failGuard, - double epsilon = DefaultEpsilon - ) - : this(new Metadata(), tactic, heuristicFunction, failGuard, epsilon) - { - } - - /// - public Goal - ( - IMetadata metadata, - ITactic tactic, - HeuristicFunction heuristicFunction, - double epsilon = DefaultEpsilon - ) : this(metadata, tactic, heuristicFunction, _ => false, epsilon) - { - } - - /// - public Goal - ( - ITactic tactic, - HeuristicFunction heuristicFunction, - double epsilon = DefaultEpsilon - ) - : this(tactic, heuristicFunction, _ => false, epsilon) - { - } - /// /// Initializes a new goal from a given tactic and a success predicate, and an optional fail-guard. /// @@ -149,19 +61,18 @@ public Goal /// but the success predicate is also satisfied, the success predicate takes precedence. /// If omitted, the goal will never fail. /// - /// - /// The goal is considered to be completed when the result of the heuristic function is below this value. - /// public Goal ( IMetadata metadata, ITactic tactic, System.Predicate predicate, - System.Predicate failGuard, - double epsilon = DefaultEpsilon + System.Predicate failGuard ) - : this(metadata, tactic, CommonHeuristicFunctions.Boolean(predicate), failGuard, epsilon) { + Metadata = metadata; + Tactic = tactic; + _predicate = predicate; + _failGuard = failGuard; } /// @@ -169,10 +80,9 @@ public Goal ( ITactic tactic, System.Predicate predicate, - System.Predicate failGuard, - double epsilon = DefaultEpsilon + System.Predicate failGuard ) - : this(new Metadata(), tactic, predicate, failGuard, epsilon) + : this(new Metadata(), tactic, predicate, failGuard) { } @@ -181,10 +91,9 @@ public Goal ( IMetadata metadata, ITactic tactic, - System.Predicate predicate, - double epsilon = DefaultEpsilon + System.Predicate predicate ) - : this(metadata, tactic, predicate, _ => false, epsilon) + : this(metadata, tactic, predicate, _ => false) { } @@ -192,43 +101,59 @@ public Goal public Goal ( ITactic tactic, - System.Predicate predicate, - double epsilon = DefaultEpsilon + System.Predicate predicate ) - : this(tactic, predicate, _ => false, epsilon) + : this(new Metadata(), tactic, predicate, _ => false) { } /// - /// Gets the of the current state of the game. - /// - /// If no heuristics have been calculated yet, they will be calculated first. - public virtual Heuristics DetermineCurrentHeuristics(TBeliefSet beliefSet) - => _heuristicFunction.Invoke(beliefSet); - - /// - /// Tests whether the goal has been achieved. - /// - /// This first checks the heuristic function of the goal. - /// When the distance of the heuristics is smaller than the epsilon of the goal, - /// the goal is considered to be completed. - /// + /// Checks whether the goal has been achieved and stores the result in . /// - /// If the heuristic function is not smaller than the epsilon, this checks the fail-guard of the goal. - /// If the fail guard returns true, the goal is considered to have failed. + /// If the predicate of the goal is satisfied, the goal is considered to have succeeded. + /// If the fail-guard is satisfied, the goal is considered to have failed. + /// If both are satisfied, the success predicate takes precedence. + /// If neither are satisfied, the goal is considered unfinished. + /// The table below summarizes the possible outcomes: + /// + /// + /// Predicate + /// Fail guard + /// Result + /// + /// + /// true + /// false + /// + /// + /// + /// true + /// true + /// + /// + /// + /// false + /// true + /// + /// + /// + /// false + /// false + /// + /// + /// /// - /// Otherwise, the goal is considered unfinished. - /// Use to get the updated value. + /// Use to get the updated value. /// - /// + /// The belief set of the agent. public virtual void UpdateStatus(TBeliefSet beliefSet) { - if (DetermineCurrentHeuristics(beliefSet).Distance < _epsilon) - Status = CompletionStatus.Success; + if (_predicate(beliefSet)) + Status = Success; else if (_failGuard(beliefSet)) - Status = CompletionStatus.Failure; + Status = Failure; else - Status = CompletionStatus.Unfinished; + Status = Unfinished; } /// diff --git a/Aplib.Core/Desire/Goals/Heuristics.cs b/Aplib.Core/Desire/Goals/Heuristics.cs deleted file mode 100644 index aa2a3fb1..00000000 --- a/Aplib.Core/Desire/Goals/Heuristics.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Aplib.Core.Desire.Goals -{ - /// - /// Contains all information on how close the associated state is to its goal. - /// Can be used to optimise search algorithms. - /// - public class Heuristics - { - /// - /// The logical distance the current state is to its goal. - /// - public float Distance { get; set; } - - /// - /// Creates a heuristic value representing just a boolean. The heuristic value is considered '0' or 'done' when - /// the boolean is true. Non-zero otherwise. - /// - /// True if completed, False if not completed. - /// - public static Heuristics Boolean(bool value) => new() { Distance = value ? 0f : 1f }; - } -} diff --git a/Aplib.Core/Desire/Goals/IGoal.cs b/Aplib.Core/Desire/Goals/IGoal.cs index cd89f9ab..e0ffcc79 100644 --- a/Aplib.Core/Desire/Goals/IGoal.cs +++ b/Aplib.Core/Desire/Goals/IGoal.cs @@ -16,12 +16,6 @@ public interface IGoal : ICompletable /// ITactic Tactic { get; } - /// - /// Gets the of the current state of the game. - /// - /// If no heuristics have been calculated yet, they will be calculated first. - Heuristics DetermineCurrentHeuristics(TBeliefSet beliefSet); - /// /// Tests whether the goal has been achieved, based on the heuristic function of the goal. /// The new completion status can be accessed via the property.