Skip to content

Commit

Permalink
feat: add Goal (#7)
Browse files Browse the repository at this point in the history
* feat: Add first draft of Goal

* chore: Improve the Goal documentation and structure

* test: Add tests for Goal

* chore: Adhere to the SonarCloud rules

* chore: Convert tabs to spaces

* chore: configure editorconfig to make files and with newline

* feat: process feedback and refactor many aspects

* fix: process pr feedback

* fix: provide usability support for boolean based heuristics

* test: new goal constructor
  • Loading branch information
SilasPeters authored Mar 17, 2024
1 parent 3cff3d3 commit f401280
Show file tree
Hide file tree
Showing 9 changed files with 398 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ tab_width = 4

# New line preferences
end_of_line = crlf
insert_final_newline = false
insert_final_newline = true

#### .NET Coding Conventions ####

Expand Down
40 changes: 40 additions & 0 deletions Aplib.Core/Desire/CommonHeuristicFunctions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;

namespace Aplib.Core.Desire
{
/// <summary>
/// Contains helper methods to generate commonly used heuristic functions.
/// </summary>
public static class CommonHeuristicFunctions
{
/// <summary>
/// Converts a boolean-based heuristic function to a <see cref="Goal.HeuristicFunction"/>.
/// </summary>
/// <param name="heuristicFunction">
/// A heuristic function which returns true only when the state is considered completed.
/// </param>
/// <returns>A heuristic function which wraps around the boolean-based heuristic function.</returns>
public static Goal.HeuristicFunction Boolean(Func<bool> heuristicFunction)
=> () => Heuristics.BooleanHeuristic(heuristicFunction.Invoke());

/// <summary>
/// A <see cref="Goal.HeuristicFunction"/> which always returns <see cref="Heuristics"/> with the same distance.
/// </summary>
/// <param name="distance">The distance which the heuristic function must always return.</param>
public static Goal.HeuristicFunction Constant(float distance) => () => new Heuristics { Distance = distance };

/// <summary>
/// Returns a heuristic function which always, at all times, and forever, returns a value indicating the state
/// can be seen as completed.
/// </summary>
/// <returns>Said heuristic function.</returns>
public static Goal.HeuristicFunction Completed() => Constant(0f);

/// <summary>
/// Returns a heuristic function which always, at all times, and forever, returns a value indicating the state
/// can be seen as NOT completed.
/// </summary>
/// <returns>Said heuristic function.</returns>
public static Goal.HeuristicFunction Uncompleted() => Constant(69_420f);
}
}
124 changes: 124 additions & 0 deletions Aplib.Core/Desire/Goal.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using System;

namespace Aplib.Core.Desire
{
/// <summary>
/// A goal effectively combines a heuristic function with a tactic, and aims to meet the heuristic function by
/// applying the tactic. Goals are combined in a <see cref="GoalStructure"/>, and are used to prepare tests or do
/// the testing.
/// </summary>
/// <seealso cref="GoalStructure"/>
public class Goal
{
/// <summary>
/// The abstract definition of what is means to test the Goal's heuristic function. Returns <see cref="Heuristics"/>, as
/// they represent how close we are to matching the heuristic function, and if the goal is completed.
/// </summary>
/// <seealso cref="Goal.Evaluate"/>
public delegate Heuristics HeuristicFunction();


/// <summary>
/// Gets the <see cref="Heuristics"/> of the current state of the game.
/// </summary>
/// <remarks>If no heuristics have been calculated yet, they will be calculated first.</remarks>
public virtual Heuristics CurrentHeuristics => _currentHeuristics ??= _heuristicFunction.Invoke();

/// <summary>
/// The name used to display the current goal during debugging, logging, or general overviews.
/// </summary>
public string Name { get; }

/// <summary>
/// The description used to describe the current goal during debugging, logging, or general overviews.
/// </summary>
public string Description { get; }

/// <summary>
/// The goal is considered to be completed, when the distance of the <see cref="CurrentHeuristics"/> is below
/// this value.
/// </summary>
protected double _epsilon { get; }


/// <summary>
/// The concrete implementation of this Goal's <see cref="HeuristicFunction"/>. Used to test whether this goal is
/// completed.
/// </summary>
/// <seealso cref="Evaluate"/>
protected HeuristicFunction _heuristicFunction;

/// <summary>
/// The <see cref="Tactic"/> used to achieve this <see cref="Goal"/>, which is executed during every iteration
/// of the BDI cycle.
/// </summary>
/// <seealso cref="Iterate()"/>
private readonly Tactic _tactic;

/// <summary>
/// The backing field of <see cref="Heuristics"/>.
/// </summary>
private Heuristics? _currentHeuristics;

/// <summary>
/// Creates a new goal which works with <see cref="Heuristics"/>.
/// </summary>
/// <param name="tactic">The tactic used to approach this goal.</param>
/// <param name="heuristicFunction">The heuristic function which defines whether a goal is reached</param>
/// <param name="name">The name of this goal, used to quickly display this goal in several contexts.</param>
/// <param name="description">The description of this goal, used to explain this goal in several contexts.</param>
/// <param name="epsilon">
/// The goal is considered to be completed, when the distance of the <see cref="CurrentHeuristics"/> is below
/// this value.
/// </param>
public Goal(Tactic tactic, HeuristicFunction heuristicFunction, string name, string description, double epsilon = 0.005d)
{
_tactic = tactic;
_heuristicFunction = heuristicFunction;
Name = name;
Description = description;
_epsilon = epsilon;
}

/// <summary>
/// Creates a new goal which works with boolean-based <see cref="Heuristics"/>.
/// </summary>
/// <param name="tactic">The tactic used to approach this goal.</param>
/// <param name="heuristicFunction">The heuristic function which defines whether a goal is reached</param>
/// <param name="name">The name of this goal, used to quickly display this goal in several contexts.</param>
/// <param name="description">The description of this goal, used to explain this goal in several contexts.</param>
/// <param name="epsilon">
/// The goal is considered to be completed, when the distance of the <see cref="CurrentHeuristics"/> is below
/// this value.
/// </param>
public Goal(Tactic tactic, Func<bool> heuristicFunction, string name, string description, double epsilon = 0.005d)
{
_tactic = tactic;
_heuristicFunction = CommonHeuristicFunctions.Boolean(heuristicFunction);
Name = name;
Description = description;
_epsilon = epsilon;
}

/// <summary>
/// Performs the next steps needed to be taken to approach this goal. Effectively this means that one BDI
/// cycle will be executed.
/// </summary>
public void Iterate()
{
_tactic.IterateBdiCycle();
}

/// <summary>
/// Tests whether the goal has been achieved, bases on the <see cref="_heuristicFunction"/> and the
/// <see cref="CurrentHeuristics"/>. When the distance of the heuristics is smaller than <see cref="_epsilon"/>,
/// the goal is considered to be completed.
/// </summary>
/// <returns>A boolean representing whether the goal is considered to be completed.</returns>
/// <seealso cref="_epsilon"/>
public bool Evaluate()
{
return CurrentHeuristics.Distance < _epsilon;
}
}
}
22 changes: 22 additions & 0 deletions Aplib.Core/Desire/Heuristics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Aplib.Core.Desire
{
/// <summary>
/// Contains all information on how close the associated state is to its goal.
/// Can be used to optimise search algorithms.
/// </summary>
public class Heuristics
{
/// <summary>
/// The logical distance the current state is to its goal.
/// </summary>
public float Distance { get; set; }

/// <summary>
/// Creates a heuristic value representing just a boolean. The heuristic value is considered '0' or 'done' when
/// the boolean is true. Non-zero otherwise.
/// </summary>
/// <param name="value">True if completed, False if not completed.</param>
/// <returns></returns>
public static Heuristics BooleanHeuristic(bool value) => new() { Distance = value ? 0f : 1f };
}
}
17 changes: 17 additions & 0 deletions Aplib.Core/Desire/Tactic.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Aplib.Core.Desire
{
/// <summary>
/// Tactics are the real meat of <see cref="Goal"/>s, as they define how the agent can approach the goal in hopes
/// of finding a solution which makes the Goal's heuristic function evaluate to being completed. A tactic represents
/// a smart combination of <see cref="Action"/>s, which are executed in a Believe Desire Intent Cycle.
/// </summary>
/// <seealso cref="Goal"/>
/// <seealso cref="Action"/>
public abstract class Tactic
{
/// <summary>
/// Execute the next cycle in the Believe Desire Intent Cycle.
/// </summary>
public abstract void IterateBdiCycle();
}
}
1 change: 1 addition & 0 deletions Aplib.Tests/Aplib.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="7.0.0-alpha.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
Expand Down
139 changes: 139 additions & 0 deletions Aplib.Tests/Desire/GoalTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
using Aplib.Core.Desire;
using Aplib.Tests.Stubs.Desire;
using Aplib.Tests.Tools;
using FluentAssertions;

namespace Aplib.Tests.Desire;

public class GoalTests
{
/// <summary>
/// Given valid parameters and metadata,
/// When the goal is constructed,
/// Then the goal should correctly store the metadata.
/// </summary>
[Fact]
public void Goal_WhenConstructed_ContainsCorrectMetaData()
{
// Arrange
Tactic tactic = new TacticStub(() => { });
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";

// Act
Goal goal = new(tactic, heuristicFunction, name, description); // Does not use helper methods on purpose

// Assert
goal.Should().NotBeNull();
goal.Name.Should().Be(name);
goal.Description.Should().Be(description);
}

/// <summary>
/// Given the Goal is created properly using its constructor,
/// When the goal has been constructed,
/// Then the given tactic has not been applied yet
/// </summary>
[Fact]
public void Goal_WhenConstructed_DidNotIterateYet()
{
// Arrange
int iterations = 0;
Tactic tactic = new TacticStub(() => iterations++);

// Act
Goal _ = new TestGoalBuilder().UseTactic(tactic).Build();

// Assert
iterations.Should().Be(0);
}

/// <summary>
/// Given the Goal is created properly using its constructor,
/// When the goal is being iterated over,
/// Then the given tactic has has been applied at least once
/// </summary>
[Fact]
public void Goal_WhenIterating_DoesIterate()
{
// Arrange
int iterations = 0;
Tactic tactic = new TacticStub(() => iterations++);

// Act
Goal goal = new TestGoalBuilder().UseTactic(tactic).Build();
goal.Iterate();

// Assert
iterations.Should().BeGreaterThan(0);
}

/// <summary>
/// Given the Goal's heuristic function is configured to have reached its goal
/// when the Evaluate() method of a goal is used,
/// then the method should return true.
/// </summary>
[Fact]
public void Goal_WhenReached_ReturnsAsCompleted()
{
// Arrange
Goal.HeuristicFunction heuristicFunction = CommonHeuristicFunctions.Completed();

// Act
Goal goal = new TestGoalBuilder().WithHeuristicFunction(heuristicFunction).Build();
bool isCompleted = goal.Evaluate();

// Assert
isCompleted.Should().Be(true);
}

/// <summary>
/// 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.
/// </summary>
[Fact]
public void Goal_WhenNotReached_DoesNotReturnAsCompleted()
{
// Arrange
Goal.HeuristicFunction heuristicFunction = CommonHeuristicFunctions.Uncompleted();

// Act
Goal goal = new TestGoalBuilder().WithHeuristicFunction(heuristicFunction).Build();
bool isCompleted = goal.Evaluate();

// Assert
isCompleted.Should().Be(false);
}

/// <summary>
/// Given the Goal's different constructors have been called with semantically equal argumetns
/// when the Evaluate() method of all goals are used,
/// then all returned values should equal.
/// </summary>
/// <param name="goalCompleted"></param>
[Theory]
[InlineData(true)]
[InlineData(false)]
public void GoalConstructor_WhereHeuristicFunctionTypeDiffers_HasEqualBehaviour(bool goalCompleted)
{
// Arrange
Tactic tactic = new TacticStub(() => { });
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";

Func<bool> heuristicFunctionBoolean = () => goalCompleted;
Goal.HeuristicFunction heuristicFunctionNonBoolean = CommonHeuristicFunctions.Boolean(() => goalCompleted);

Goal goalBoolean = new(tactic, heuristicFunctionBoolean, name, description);
Goal goalNonBoolean = new(tactic, heuristicFunctionNonBoolean, name, description);

// Act
bool goalBooleanEvaluation = goalBoolean.Evaluate();
bool goalNonBooleanEvaluation = goalNonBoolean.Evaluate();

// Assert
goalBooleanEvaluation.Should().Be(goalNonBooleanEvaluation);
}
}
16 changes: 16 additions & 0 deletions Aplib.Tests/Stubs/Desire/TacticStub.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Aplib.Core.Desire;

namespace Aplib.Tests.Stubs.Desire;

/// <summary>
/// A fake tactic, which is just a wrapper around the <see cref="Action"/> you define as argument.
/// </summary>
/// <param name="iteration">The method to be executed during iteration.</param>
internal class TacticStub(Action iteration) : Tactic
{
/// <inheritdoc />
public override void IterateBdiCycle()
{
iteration.Invoke();
}
}
Loading

0 comments on commit f401280

Please sign in to comment.