From 33ea7e2cd933baea9206ae962b56309f9d6faead Mon Sep 17 00:00:00 2001 From: Silas Peters <69711739+SilasPeters@users.noreply.github.com> Date: Mon, 1 Apr 2024 15:41:22 +0200 Subject: [PATCH] feat: add GoalStructure (#25) * wip: overgave aan Thijs * feat: implement GoalStructure * chore: fix .editorconfig * feat: add interfaces for easy testing * feat: simplify goal structure * feat: make Goal and Goalstructure inherit from interfaces. * feat: change implementation of goal structures. The goal structures have been altered so that they now first update the state, and then choose a goal. They also have a mechanism for interrupting and reinstating them. * test: test the goal structures, and rework some of the tests to work with moq * chore: conform to sonar * fix: conform to Jens * test: fix tests * feat: make goal structure type contravariant * feat: remove interruptable * docs: fix the wording of CompletionStatus.cs * chore: conform to sonar * fix: improve GoalStructure tests * refactor: rename State to Status * fix: conform to tboefijn Co-authored-by: Thijs Boerefijn <51719415+Tboefijn@users.noreply.github.com> * fix: conform to tboefijn, again Co-authored-by: Thijs Boerefijn <51719415+Tboefijn@users.noreply.github.com> --------- Co-authored-by: Joachim Dekker Co-authored-by: Jens Steenmetz Co-authored-by: Thijs Boerefijn <51719415+Tboefijn@users.noreply.github.com> --- .editorconfig | 72 ++-- Aplib.Core/CompletionStatus.cs | 23 ++ Aplib.Core/Desire/FirstOfGoalStructure.cs | 80 ++++ Aplib.Core/Desire/GoalStructure.cs | 44 +++ Aplib.Core/Desire/Goals/Goal.cs | 80 ++-- Aplib.Core/Desire/Goals/IGoal.cs | 32 ++ Aplib.Core/Desire/IGoalStructure.cs | 29 ++ Aplib.Core/Desire/PrimitiveGoalStructure.cs | 34 ++ Aplib.Core/Desire/RepeatGoalStructure.cs | 51 +++ Aplib.Core/Desire/SequentialGoalStructure.cs | 89 +++++ Aplib.Core/ICompletable.cs | 13 + Aplib.Tests/Aplib.Tests.csproj | 59 +-- Aplib.Tests/Core/Desire/GoalStructureTests.cs | 373 ++++++++++++++++++ Aplib.Tests/{ => Core}/Desire/GoalTests.cs | 111 +++--- .../Core/{ => Intent/Actions}/ActionTests.cs | 92 ++--- .../Core/{ => Intent}/Tactics/TacticTests.cs | 83 ++-- Aplib.Tests/Stubs/Desire/TacticStub.cs | 14 - Aplib.Tests/Tools/TestGoalBuilder.cs | 10 +- 18 files changed, 1025 insertions(+), 264 deletions(-) create mode 100644 Aplib.Core/CompletionStatus.cs create mode 100644 Aplib.Core/Desire/FirstOfGoalStructure.cs create mode 100644 Aplib.Core/Desire/GoalStructure.cs create mode 100644 Aplib.Core/Desire/Goals/IGoal.cs create mode 100644 Aplib.Core/Desire/IGoalStructure.cs create mode 100644 Aplib.Core/Desire/PrimitiveGoalStructure.cs create mode 100644 Aplib.Core/Desire/RepeatGoalStructure.cs create mode 100644 Aplib.Core/Desire/SequentialGoalStructure.cs create mode 100644 Aplib.Core/ICompletable.cs create mode 100644 Aplib.Tests/Core/Desire/GoalStructureTests.cs rename Aplib.Tests/{ => Core}/Desire/GoalTests.cs (75%) rename Aplib.Tests/Core/{ => Intent/Actions}/ActionTests.cs (92%) rename Aplib.Tests/Core/{ => Intent}/Tactics/TacticTests.cs (87%) delete mode 100644 Aplib.Tests/Stubs/Desire/TacticStub.cs diff --git a/.editorconfig b/.editorconfig index 74388033..01cce039 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,12 +1,12 @@ # Remove the line below if you want to inherit .editorconfig settings from higher directories root = true -# XMLDOC settings -xmldoc_indent_size = 0 - # C# files [*.cs] +# XMLDoc settings +resharper_xmldoc_indent_size = 0 + #### Core EditorConfig Options #### # Indentation and spacing @@ -107,7 +107,7 @@ csharp_style_conditional_delegate_call = true # Modifier preferences csharp_prefer_static_local_function = true -csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_preferred_modifier_order = public, private, protected, internal, file, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, required, volatile, async csharp_style_prefer_readonly_struct = true csharp_style_prefer_readonly_struct_member = true @@ -195,6 +195,10 @@ csharp_preserve_single_line_statements = true # Naming rules +dotnet_naming_rule.private_members_with_underscore.severity = warning +dotnet_naming_rule.private_members_with_underscore.symbols = private_fields +dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore + dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i @@ -203,64 +207,58 @@ dotnet_naming_rule.types_should_be_pascal_case.severity = warning dotnet_naming_rule.types_should_be_pascal_case.symbols = types dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case +dotnet_naming_rule.classes_should_be_pascal_case.severity = warning +dotnet_naming_rule.classes_should_be_pascal_case.symbols = classes +dotnet_naming_rule.classes_should_be_pascal_case.style = pascal_case + dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = warning dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case -dotnet_naming_rule.private_members_with_underscore.severity = warning -dotnet_naming_rule.private_members_with_underscore.symbols = private_fields -dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore +dotnet_naming_rule.public_fields_should_be_pascal_case.severity = warning +dotnet_naming_rule.public_fields_should_be_pascal_case.symbols = public_fields +dotnet_naming_rule.public_fields_should_be_pascal_case.style = pascal_case # Symbol specifications dotnet_naming_symbols.interface.applicable_kinds = interface dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interface.required_modifiers = +dotnet_naming_symbols.interface.required_modifiers = dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum, field dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types.required_modifiers = +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.classes.applicable_kinds = class, struct, enum +dotnet_naming_symbols.classes.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.classes.required_modifiers = dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = +dotnet_naming_symbols.non_field_members.required_modifiers = -dotnet_naming_symbols.private_fields.applicable_kinds = field, property -dotnet_naming_symbols.private_fields.applicable_accessibilities = private +dotnet_naming_symbols.public_fields.applicable_kinds = field, property +dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal, protected_internal + +dotnet_naming_symbols.private_fields.applicable_kinds = field, property +dotnet_naming_symbols.private_fields.applicable_accessibilities = protected, private_protected, private # Naming styles -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case + dotnet_naming_style.prefix_underscore.capitalization = camel_case dotnet_naming_style.prefix_underscore.required_prefix = _ -[*.{cs,vb}] -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_null_propagation = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion -dotnet_style_prefer_auto_properties = true:silent -dotnet_style_object_initializer = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_operator_placement_when_wrapping = beginning_of_line -tab_width = 4 -indent_size = 4 -end_of_line = crlf - # Use underscores for private fields -dotnet_naming_symbols.private_fields.applicable_kinds = field -dotnet_naming_symbols.private_fields.applicable_accessibilities = internal, protected_internal, private - -dotnet_naming_style.prefix_underscore.capitalization = camel_case -dotnet_naming_style.prefix_underscore.required_prefix = _ - -dotnet_naming_rule.private_fields_with_underscore.symbols = private_fields -dotnet_naming_rule.private_fields_with_underscore.style = prefix_underscore +dotnet_naming_rule.private_fields_with_underscore.symbols = private_fields +dotnet_naming_rule.private_fields_with_underscore.style = prefix_underscore dotnet_naming_rule.private_fields_with_underscore.severity = warning \ No newline at end of file diff --git a/Aplib.Core/CompletionStatus.cs b/Aplib.Core/CompletionStatus.cs new file mode 100644 index 00000000..265f953f --- /dev/null +++ b/Aplib.Core/CompletionStatus.cs @@ -0,0 +1,23 @@ +namespace Aplib.Core +{ + /// + /// Represents the state of a completable object. + /// + public enum CompletionStatus + { + /// + /// Represents the status of a completable object that is not yet completed. + /// + Unfinished, + + /// + /// Represents the status of a completable object that has been successfully completed. + /// + Success, + + /// + /// Represents the status of a completable object that has failed to complete. + /// + Failure + } +} diff --git a/Aplib.Core/Desire/FirstOfGoalStructure.cs b/Aplib.Core/Desire/FirstOfGoalStructure.cs new file mode 100644 index 00000000..335c0034 --- /dev/null +++ b/Aplib.Core/Desire/FirstOfGoalStructure.cs @@ -0,0 +1,80 @@ +using Aplib.Core.Belief; +using Aplib.Core.Desire.Goals; +using System; +using System.Collections.Generic; + +namespace Aplib.Core.Desire +{ + /// + /// Represents a goal structure that will complete if any of its children complete. + /// + /// + /// The children of this goal structure will be executed in the order they are given. + /// + /// The beliefset of the agent. + public class FirstOfGoalStructure : GoalStructure, IDisposable + where TBeliefSet : IBeliefSet + { + private IEnumerator> _childrenEnumerator { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The children of the goal structure. + public FirstOfGoalStructure(IList> children) : base(children) + { + _childrenEnumerator = children.GetEnumerator(); + _childrenEnumerator.MoveNext(); + _currentGoalStructure = _childrenEnumerator.Current; + } + + /// + public override IGoal GetCurrentGoal(TBeliefSet beliefSet) => _currentGoalStructure!.GetCurrentGoal(beliefSet); + + /// + public override void UpdateStatus(TBeliefSet beliefSet) + { + // Loop through all the children until one of them is unfinished or successful. + // This loop is here to prevent tail recursion. + while (true) + { + if (Status == CompletionStatus.Success) return; + _currentGoalStructure!.UpdateStatus(beliefSet); + + switch (_currentGoalStructure.Status) + { + case CompletionStatus.Unfinished: + return; + case CompletionStatus.Success: + Status = CompletionStatus.Success; + return; + } + + if (_childrenEnumerator.MoveNext()) + { + _currentGoalStructure = _childrenEnumerator.Current; + Status = CompletionStatus.Unfinished; + + // Update the Status of the new goal structure + continue; + } + + Status = CompletionStatus.Failure; + return; + } + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes of the goal structure. + /// + /// Whether we are actually disposing. + protected virtual void Dispose(bool disposing) => _childrenEnumerator.Dispose(); + } +} diff --git a/Aplib.Core/Desire/GoalStructure.cs b/Aplib.Core/Desire/GoalStructure.cs new file mode 100644 index 00000000..f9e0f54d --- /dev/null +++ b/Aplib.Core/Desire/GoalStructure.cs @@ -0,0 +1,44 @@ +using Aplib.Core.Belief; +using Aplib.Core.Desire.Goals; +using System.Collections.Generic; + +namespace Aplib.Core.Desire +{ + /// + /// Describes a structure of goals that need to be fulfilled. + /// + public abstract class GoalStructure : IGoalStructure where TBeliefSet : IBeliefSet + { + /// + public CompletionStatus Status { get; protected set; } + + /// + /// The children of the goal structure. + /// + protected readonly IEnumerable> _children; + + /// + /// The goal structure that is currently being fulfilled. + /// + protected IGoalStructure? _currentGoalStructure; + + /// + /// Initializes a new instance of the class. + /// + /// The children of the goal structure. + protected GoalStructure(IEnumerable> children) => _children = children; + + /// + /// Gets the current goal using the given . + /// + /// The belief set of the agent. + /// The current goal to be fulfilled. + public abstract IGoal GetCurrentGoal(TBeliefSet beliefSet); + + /// + /// Updates the state of the goal structure. + /// + /// The belief set of the agent. + public abstract void UpdateStatus(TBeliefSet beliefSet); + } +} diff --git a/Aplib.Core/Desire/Goals/Goal.cs b/Aplib.Core/Desire/Goals/Goal.cs index 0fca6422..5c9d8130 100644 --- a/Aplib.Core/Desire/Goals/Goal.cs +++ b/Aplib.Core/Desire/Goals/Goal.cs @@ -6,26 +6,24 @@ namespace Aplib.Core.Desire.Goals { /// /// 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 , and are used to prepare tests or do + /// applying the tactic. Goals are combined in a , and are used to prepare tests + /// or do /// the testing. /// - /// - public class Goal + /// + public class Goal : IGoal { /// - /// The abstract definition of what is means to test the Goal's heuristic function. Returns , as + /// 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(BeliefSet beliefSet); - + /// + public delegate Heuristics HeuristicFunction(IBeliefSet beliefSet); /// - /// Gets the of the current state of the game. + /// The description used to describe the current goal during debugging, logging, or general overviews. /// - /// If no heuristics have been calculated yet, they will be calculated first. - public virtual Heuristics CurrentHeuristics(BeliefSet beliefSet) - => _currentHeuristics ??= _heuristicFunction.Invoke(beliefSet); + public string Description { get; } /// /// The name used to display the current goal during debugging, logging, or general overviews. @@ -33,47 +31,44 @@ public virtual Heuristics CurrentHeuristics(BeliefSet beliefSet) public string Name { get; } /// - /// The description used to describe the current goal during debugging, logging, or general overviews. + /// The used to achieve this , which is executed during every + /// iteration of the BDI cycle. /// - public string Description { get; } + public Tactic Tactic { get; } + + /// + public CompletionStatus Status { get; protected set; } /// - /// The goal is considered to be completed, when the distance of the is below + /// The goal is considered to be completed, when the distance of the is below /// this value. /// protected double _epsilon { get; } - - /// - /// The concrete implementation of this Goal's . Used to test whether this goal is + /// The concrete implementation of this Goal's . Used to test whether this goal is /// completed. /// - /// + /// protected HeuristicFunction _heuristicFunction; /// - /// The used to achieve this , which is executed during every iteration - /// of the BDI cycle. - /// - public Tactic Tactic { get; private set; } - - /// - /// The backing field of . + /// The backing field of . /// private Heuristics? _currentHeuristics; /// - /// Creates a new goal which works with . + /// Creates a new goal which works with . /// /// The tactic used to approach this goal. /// The heuristic function which defines whether a goal is reached /// The name of this goal, used to quickly display this goal in several contexts. /// The description of this goal, used to explain this goal in several contexts. /// - /// The goal is considered to be completed, when the distance of the is below + /// The goal is considered to be completed, when the distance of the is below /// this value. /// - public Goal(Tactic tactic, HeuristicFunction heuristicFunction, string name, string description, double epsilon = 0.005d) + public Goal(Tactic tactic, HeuristicFunction heuristicFunction, string name, string description, + double epsilon = 0.005d) { Tactic = tactic; _heuristicFunction = heuristicFunction; @@ -83,14 +78,14 @@ public Goal(Tactic tactic, HeuristicFunction heuristicFunction, string name, str } /// - /// Creates a new goal which works with boolean-based . + /// Creates a new goal which works with boolean-based . /// /// The tactic used to approach this goal. - /// The heuristic function (or specifically predicate) which defines whether a goal is reached + /// The heuristic function (or specifically predicate) which defines whether a goal is reached. /// The name of this goal, used to quickly display this goal in several contexts. /// The description of this goal, used to explain this goal in several contexts. /// - /// The goal is considered to be completed, when the distance of the is below + /// The goal is considered to be completed, when the distance of the is below /// this value. /// public Goal(Tactic tactic, Func predicate, string name, string description, double epsilon = 0.005d) @@ -103,12 +98,25 @@ public Goal(Tactic tactic, Func predicate, string name, string description } /// - /// Tests whether the goal has been achieved, bases on the and the - /// . When the distance of the heuristics is smaller than , + /// Gets the of the current state of the game. + /// + /// If no heuristics have been calculated yet, they will be calculated first. + public virtual Heuristics CurrentHeuristics(IBeliefSet beliefSet) + => _currentHeuristics ??= _heuristicFunction.Invoke(beliefSet); + + /// + /// Tests whether the goal has been achieved, bases on the and the + /// . When the distance of the heuristics is smaller than , /// the goal is considered to be completed. /// - /// A boolean representing whether the goal is considered to be completed. - /// - public bool IsCompleted(BeliefSet beliefSet) => CurrentHeuristics(beliefSet).Distance < _epsilon; + /// An enum representing whether the goal is complete and if so, with what result. + /// + public virtual CompletionStatus GetStatus(IBeliefSet beliefSet) + { + Status = CurrentHeuristics(beliefSet).Distance < _epsilon + ? CompletionStatus.Success + : CompletionStatus.Unfinished; + return Status; + } } } diff --git a/Aplib.Core/Desire/Goals/IGoal.cs b/Aplib.Core/Desire/Goals/IGoal.cs new file mode 100644 index 00000000..9fb23eaa --- /dev/null +++ b/Aplib.Core/Desire/Goals/IGoal.cs @@ -0,0 +1,32 @@ +using Aplib.Core.Belief; +using Aplib.Core.Intent.Tactics; + +namespace Aplib.Core.Desire.Goals +{ + /// + /// Defines a goal that can be achieved by a . + /// + public interface IGoal : ICompletable + { + /// + /// The used to achieve this , which is executed during every + /// iteration of the BDI cycle. + /// + Tactic Tactic { get; } + + /// + /// Gets the of the current state of the game. + /// + /// If no heuristics have been calculated yet, they will be calculated first. + Heuristics CurrentHeuristics(IBeliefSet beliefSet); + + /// + /// Tests whether the goal has been achieved, bases on the and the + /// . When the distance of the heuristics is smaller than + /// , the goal is considered to be completed. + /// + /// An enum representing whether the goal is complete and if so, with what result. + /// + CompletionStatus GetStatus(IBeliefSet beliefSet); + } +} diff --git a/Aplib.Core/Desire/IGoalStructure.cs b/Aplib.Core/Desire/IGoalStructure.cs new file mode 100644 index 00000000..66be3923 --- /dev/null +++ b/Aplib.Core/Desire/IGoalStructure.cs @@ -0,0 +1,29 @@ +using Aplib.Core.Belief; +using Aplib.Core.Desire.Goals; + +namespace Aplib.Core.Desire +{ + /// + /// Represents a goal structure. + /// + /// + /// A goal structure is a structure of predicates that must be fulfilled in order to complete a test. + /// + /// The belief set of the agent. + public interface IGoalStructure : ICompletable + where TBeliefSet : IBeliefSet + { + /// + /// Gets the current goal using the given . + /// + /// The belief set of the agent. + /// The current goal to be fulfilled. + IGoal GetCurrentGoal(TBeliefSet beliefSet); + + /// + /// Updates the state of the goal structure. + /// + /// The belief set of the agent. + void UpdateStatus(TBeliefSet beliefSet); + } +} diff --git a/Aplib.Core/Desire/PrimitiveGoalStructure.cs b/Aplib.Core/Desire/PrimitiveGoalStructure.cs new file mode 100644 index 00000000..530cbae2 --- /dev/null +++ b/Aplib.Core/Desire/PrimitiveGoalStructure.cs @@ -0,0 +1,34 @@ +using Aplib.Core.Belief; +using Aplib.Core.Desire.Goals; +using System; + +namespace Aplib.Core.Desire +{ + /// + /// Represents a goal structure that will complete if any of its children complete. + /// + /// + /// This is the most primitive goal structure. It is used to represent a single goal that is not part of a larger + /// structure. + /// This goal structure will only return the goal it was created with if the goal is not yet finished. + /// + /// The beliefset of the agent. + public class PrimitiveGoalStructure : GoalStructure + where TBeliefSet : IBeliefSet + { + private readonly IGoal _goal; + + /// + /// Initializes a new instance of the class. + /// + /// The goal to fulfill. + public PrimitiveGoalStructure(IGoal goal) : base(Array.Empty>()) => _goal = goal; + + /// + public override IGoal GetCurrentGoal(TBeliefSet beliefSet) => _goal; + + /// + public override void UpdateStatus(TBeliefSet beliefSet) => + Status = _goal.GetStatus(beliefSet); + } +} diff --git a/Aplib.Core/Desire/RepeatGoalStructure.cs b/Aplib.Core/Desire/RepeatGoalStructure.cs new file mode 100644 index 00000000..6d041e0f --- /dev/null +++ b/Aplib.Core/Desire/RepeatGoalStructure.cs @@ -0,0 +1,51 @@ +using Aplib.Core.Belief; +using Aplib.Core.Desire.Goals; +using System.Collections.Generic; +using static Aplib.Core.CompletionStatus; + +namespace Aplib.Core.Desire +{ + /// + /// Represents a goal structure that will complete if any of its children complete. + /// + /// + /// This structure will repeatedly execute the goal it was created with until the goal is finished. + /// + /// The beliefset of the agent. + public class RepeatGoalStructure : GoalStructure + where TBeliefSet : IBeliefSet + { + /// + /// Initializes a new instance of the class. + /// + /// The goalstructure to repeat + public RepeatGoalStructure(IGoalStructure goalStructure) : base( + new List> { goalStructure }) => + _currentGoalStructure = goalStructure; + + /// + public override IGoal GetCurrentGoal(TBeliefSet beliefSet) => _currentGoalStructure!.Status switch + { + Unfinished or Failure => _currentGoalStructure.GetCurrentGoal(beliefSet), + _ => FinishRepeat(beliefSet) + }; + + /// + public override void UpdateStatus(TBeliefSet beliefSet) + { + _currentGoalStructure!.UpdateStatus(beliefSet); + + Status = _currentGoalStructure.Status switch + { + Failure or Unfinished => Unfinished, + _ => Success + }; + } + + private IGoal FinishRepeat(TBeliefSet beliefSet) + { + Status = Success; + return _currentGoalStructure!.GetCurrentGoal(beliefSet); + } + } +} diff --git a/Aplib.Core/Desire/SequentialGoalStructure.cs b/Aplib.Core/Desire/SequentialGoalStructure.cs new file mode 100644 index 00000000..57d660a4 --- /dev/null +++ b/Aplib.Core/Desire/SequentialGoalStructure.cs @@ -0,0 +1,89 @@ +using Aplib.Core.Belief; +using Aplib.Core.Desire.Goals; +using System; +using System.Collections.Generic; + +namespace Aplib.Core.Desire +{ + /// + /// Represents a sequential goal structure. + /// + /// + /// This class is a specific type of goal structure where goals are processed sequentially. + /// All goals must be completed in order for the goal structure to be completed. + /// + /// The type of belief set that this goal structure operates on. + public class SequentialGoalStructure : GoalStructure, IDisposable + where TBeliefSet : IBeliefSet + { + /// + /// Gets or sets the enumerator for the children of the goal structure. + /// + private IEnumerator> _childrenEnumerator { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The children of the goal structure. + public SequentialGoalStructure(IList> children) : base(children) + { + if (children.Count <= 0) + throw new ArgumentException("Collection of children is empty", nameof(children)); + _childrenEnumerator = _children.GetEnumerator(); + _childrenEnumerator.MoveNext(); + _currentGoalStructure = _childrenEnumerator.Current; + } + + /// + public override IGoal GetCurrentGoal(TBeliefSet beliefSet) => _currentGoalStructure!.GetCurrentGoal(beliefSet); + + /// + public override void UpdateStatus(TBeliefSet beliefSet) + { + // Loop through all the children until one of them is unfinished or successful. + // This loop is here to prevent tail recursion. + while (true) + { + if (Status == CompletionStatus.Success) return; + _currentGoalStructure!.UpdateStatus(beliefSet); + + switch (_currentGoalStructure.Status) + { + case CompletionStatus.Unfinished: + return; + case CompletionStatus.Failure: + Status = CompletionStatus.Failure; + return; + case CompletionStatus.Success: + default: + break; + } + + if (_childrenEnumerator.MoveNext()) + { + _currentGoalStructure = _childrenEnumerator.Current; + Status = CompletionStatus.Unfinished; + + // Update the state of the new goal structure + continue; + } + + Status = CompletionStatus.Success; + return; + } + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes the enumerator. + /// + /// Whether the object is being disposed. + protected virtual void Dispose(bool disposing) => _childrenEnumerator.Dispose(); + } +} diff --git a/Aplib.Core/ICompletable.cs b/Aplib.Core/ICompletable.cs new file mode 100644 index 00000000..7843d330 --- /dev/null +++ b/Aplib.Core/ICompletable.cs @@ -0,0 +1,13 @@ +namespace Aplib.Core +{ + /// + /// Defines an object that can be completed. + /// + public interface ICompletable + { + /// + /// Gets the completion status of the object. + /// + public CompletionStatus Status { get; } + } +} diff --git a/Aplib.Tests/Aplib.Tests.csproj b/Aplib.Tests/Aplib.Tests.csproj index b2470e8f..0d591c19 100644 --- a/Aplib.Tests/Aplib.Tests.csproj +++ b/Aplib.Tests/Aplib.Tests.csproj @@ -1,37 +1,38 @@ - - net8.0 - enable - enable + + net8.0 + enable + enable - false - true - + false + true + - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + - - - all - runtime; build; native; contentfiles; analyzers - - + + + all + runtime; build; native; contentfiles; analyzers + + - - - + + + diff --git a/Aplib.Tests/Core/Desire/GoalStructureTests.cs b/Aplib.Tests/Core/Desire/GoalStructureTests.cs new file mode 100644 index 00000000..0dafbe2a --- /dev/null +++ b/Aplib.Tests/Core/Desire/GoalStructureTests.cs @@ -0,0 +1,373 @@ +using Aplib.Core; +using Aplib.Core.Belief; +using Aplib.Core.Desire; +using Aplib.Core.Desire.Goals; +using FluentAssertions; +using Moq; +using Moq.Protected; + +namespace Aplib.Tests.Core.Desire; + +public class GoalStructureTests +{ + [Fact] + public void FirstOfGoalStructure_WhenAllGoalsFail_ShouldReturnFailure() + { + Mock> goalStructure1 = new(); + goalStructure1.SetupGet(g => g.Status).Returns(CompletionStatus.Failure); + Mock> goalStructure2 = new(); + goalStructure2.SetupGet(g => g.Status).Returns(CompletionStatus.Failure); + + BeliefSet beliefSet = Mock.Of(); + FirstOfGoalStructure firstOfGoalStructure = new(new List> + { + goalStructure1.Object, goalStructure2.Object + }); + + // Act + firstOfGoalStructure.UpdateStatus(beliefSet); + + // Assert + firstOfGoalStructure.Status.Should().Be(CompletionStatus.Failure); + } + + [Fact] + public void FirstOfGoalStructure_WhenDisposing_ShouldDisposeChildren() + { + // Arrange + Mock> goalStructure1 = new(); + Mock> goalStructure2 = new(); + + Mock> firstOfGoalStructure = + new(new List> { goalStructure1.Object, goalStructure2.Object }) + { + CallBase = true + }; + + // Act + firstOfGoalStructure.Object.Dispose(); + + // Assert + firstOfGoalStructure.Protected().Verify("Dispose", Times.Once(), ItExpr.IsAny()); + } + + [Fact] + public void FirstOfGoalStructure_WhenFinished_ShouldEarlyExit() + { + Mock> goalStructure1 = new(); + goalStructure1.SetupGet(g => g.Status).Returns(CompletionStatus.Success); + + Mock> goalStructure2 = new(); + goalStructure2.SetupGet(g => g.Status).Returns(CompletionStatus.Success); + + BeliefSet beliefSet = Mock.Of(); + FirstOfGoalStructure firstOfGoalStructure = new(new List> + { + goalStructure1.Object, goalStructure2.Object + }); + + // Act + firstOfGoalStructure.UpdateStatus(beliefSet); + firstOfGoalStructure.UpdateStatus(beliefSet); + + // Assert + firstOfGoalStructure.Status.Should().Be(CompletionStatus.Success); + goalStructure1.Verify(x => x.UpdateStatus(It.IsAny()), Times.Once); + goalStructure2.Verify(x => x.UpdateStatus(It.IsAny()), Times.Never); + } + + [Fact] + public void FirstOfGoalStructure_WhenFirstGoalIsFailure_ShouldReturnSecondGoal() + { + // Arrange + Mock> goalStructure1 = new(); + goalStructure1.SetupGet(g => g.Status).Returns(CompletionStatus.Failure); + Mock> goalStructure2 = new(); + IGoal goal = Mock.Of(); + goalStructure2 + .Setup(g => g.GetCurrentGoal(It.IsAny())) + .Returns(goal); + + BeliefSet beliefSet = Mock.Of(); + FirstOfGoalStructure firstOfGoalStructure = new(new List> + { + goalStructure1.Object, goalStructure2.Object + }); + + // Act + firstOfGoalStructure.UpdateStatus(beliefSet); + IGoal currentGoal = firstOfGoalStructure.GetCurrentGoal(beliefSet); + + // Assert + firstOfGoalStructure.Status.Should().Be(CompletionStatus.Unfinished); + currentGoal.Should().Be(goal); + } + + [Fact] + public void FirstOfGoalStructure_WhenFirstGoalIsFinished_ShouldReturnSuccess() + { + // Arrange + Mock> goalStructure1 = new(); + goalStructure1.SetupGet(g => g.Status).Returns(CompletionStatus.Success); + Mock> goalStructure2 = new(); + BeliefSet beliefSet = Mock.Of(); + FirstOfGoalStructure firstOfGoalStructure = new(new List> + { + goalStructure1.Object, goalStructure2.Object + }); + + // Act + firstOfGoalStructure.UpdateStatus(beliefSet); + + // Assert + firstOfGoalStructure.Status.Should().Be(CompletionStatus.Success); + } + + [Fact] + public void FirstOfGoalStructure_WhenFirstGoalIsUnfinished_ShouldReturnUnfinished() + { + // Arrange + Mock goal = new(); + Mock> goalStructure1 = new(); + goalStructure1 + .Setup(g => g.GetCurrentGoal(It.IsAny())) + .Returns(goal.Object); + + goalStructure1 + .SetupGet(g => g.Status) + .Returns(CompletionStatus.Unfinished); + + Mock> goalStructure2 = new(); + BeliefSet beliefSet = Mock.Of(); + FirstOfGoalStructure firstOfGoalStructure = new(new List> + { + goalStructure1.Object, goalStructure2.Object + }); + + // Act + firstOfGoalStructure.UpdateStatus(beliefSet); + IGoal currentGoal = firstOfGoalStructure.GetCurrentGoal(beliefSet); + + // Assert + firstOfGoalStructure.Status.Should().Be(CompletionStatus.Unfinished); + currentGoal.Should().Be(goal.Object); + } + + [Fact] + public void FirstOfGoalStructure_WhenGoalIsUnfinished_ShouldReturnGoal() + { + Mock> goalStructure1 = new(); + goalStructure1.SetupGet(g => g.Status).Returns(CompletionStatus.Unfinished); + + IGoal goal = Mock.Of(); + goalStructure1.Setup(g => g.GetCurrentGoal(It.IsAny())).Returns(goal); + + Mock> goalStructure2 = new(); + goalStructure2.SetupGet(g => g.Status).Returns(CompletionStatus.Unfinished); + + BeliefSet beliefSet = Mock.Of(); + FirstOfGoalStructure firstOfGoalStructure = new(new List> + { + goalStructure1.Object, goalStructure2.Object + }); + + // Act + firstOfGoalStructure.UpdateStatus(beliefSet); + IGoal currentGoal = firstOfGoalStructure.GetCurrentGoal(beliefSet); + + // Assert + firstOfGoalStructure.Status.Should().Be(CompletionStatus.Unfinished); + currentGoal.Should().Be(goal); + } + + [Theory] + [InlineData(CompletionStatus.Success, CompletionStatus.Success)] + [InlineData(CompletionStatus.Failure, CompletionStatus.Failure)] + [InlineData(CompletionStatus.Unfinished, CompletionStatus.Unfinished)] + public void PrimitiveGoalStructure_WhenGoalHasState_ShouldHaveSameState(CompletionStatus status, + CompletionStatus expected) + { + // Arrange + Mock goal = new(); + goal.Setup(g => g.GetStatus(It.IsAny())).Returns(status); + BeliefSet beliefSet = Mock.Of(); + PrimitiveGoalStructure primitiveGoalStructure = new(goal.Object); + + // Act + primitiveGoalStructure.UpdateStatus(beliefSet); + + // Assert + primitiveGoalStructure.Status.Should().Be(expected); + } + + [Fact] + public void PrimitiveGoalStructure_WhenGoalIsNotFinished_ShouldReturnGoal() + { + // Arrange + Mock goal = new(); + goal.Setup(g => g.GetStatus(It.IsAny())).Returns(CompletionStatus.Unfinished); + BeliefSet beliefSet = Mock.Of(); + PrimitiveGoalStructure primitiveGoalStructure = new(goal.Object); + + // Act + primitiveGoalStructure.UpdateStatus(beliefSet); + IGoal currentGoal = primitiveGoalStructure.GetCurrentGoal(beliefSet); + + // Assert + currentGoal.Should().Be(goal.Object); + } + + [Fact] + public void RepeatGoalStructure_WhenGoalIsNotFinished_ShouldReturnGoal() + { + // Arrange + Mock goal = new(); + goal.Setup(g => g.GetStatus(It.IsAny())).Returns(CompletionStatus.Unfinished); + BeliefSet beliefSet = Mock.Of(); + PrimitiveGoalStructure primitiveGoalStructure = new(goal.Object); + RepeatGoalStructure repeatGoalStructure = new(primitiveGoalStructure); + + // Act + repeatGoalStructure.UpdateStatus(beliefSet); + IGoal currentGoal = repeatGoalStructure.GetCurrentGoal(beliefSet); + + // Assert + repeatGoalStructure.Status.Should().Be(CompletionStatus.Unfinished); + currentGoal.Should().Be(goal.Object); + } + + [Fact] + public void RepeatGoalStructure_WhenGoalStructureHasFailed_ShouldReturnGoal() + { + // Arrange + Mock goal = new(); + goal.Setup(g => g.GetStatus(It.IsAny())).Returns(CompletionStatus.Failure); + BeliefSet beliefSet = Mock.Of(); + PrimitiveGoalStructure primitiveGoalStructure = new(goal.Object); + RepeatGoalStructure repeatGoalStructure = new(primitiveGoalStructure); + + // Act + repeatGoalStructure.UpdateStatus(beliefSet); + IGoal currentGoal = repeatGoalStructure.GetCurrentGoal(beliefSet); + + // Assert + repeatGoalStructure.Status.Should().Be(CompletionStatus.Unfinished); + currentGoal.Should().Be(goal.Object); + } + + [Fact] + public void RepeatGoalStructure_WhenGoalStructureHasSucceeded_ShouldSucceed() + { + // Arrange + Mock goal = new(); + goal.Setup(g => g.GetStatus(It.IsAny())).Returns(CompletionStatus.Success); + BeliefSet beliefSet = Mock.Of(); + PrimitiveGoalStructure primitiveGoalStructure = new(goal.Object); + RepeatGoalStructure repeatGoalStructure = new(primitiveGoalStructure); + + // Act + repeatGoalStructure.UpdateStatus(beliefSet); + IGoal currentGoal = repeatGoalStructure.GetCurrentGoal(beliefSet); + + // Assert + repeatGoalStructure.Status.Should().Be(CompletionStatus.Success); + currentGoal.Should().Be(goal.Object); + } + + [Fact] + public void SequentialGoalStructure_WhenDisposing_ShouldDisposeChildren() + { + // Arrange + Mock> goalStructure1 = new(); + Mock> goalStructure2 = new(); + + Mock> sequentialGoalStructure = + new(new List> { goalStructure1.Object, goalStructure2.Object }) + { + CallBase = true + }; + + // Act + sequentialGoalStructure.Object.Dispose(); + + // Assert + sequentialGoalStructure.Protected().Verify("Dispose", Times.Once(), ItExpr.IsAny()); + } + + [Fact] + public void SequentialGoalStructure_WhenFirstGoalIsFinished_ShouldNotUseFirstGoalAgain() + { + Mock> goalStructure1 = new(); + goalStructure1.SetupGet(g => g.Status).Returns(CompletionStatus.Success); + + Mock> goalStructure2 = new(); + goalStructure2.SetupGet(g => g.Status).Returns(CompletionStatus.Success); + + BeliefSet beliefSet = Mock.Of(); + SequentialGoalStructure sequentialGoalStructure = new(new List> + { + goalStructure1.Object, goalStructure2.Object + }); + + // Act + sequentialGoalStructure.UpdateStatus(beliefSet); + sequentialGoalStructure.UpdateStatus(beliefSet); + + // Assert + sequentialGoalStructure.Status.Should().Be(CompletionStatus.Success); + goalStructure1.Verify(x => x.UpdateStatus(It.IsAny()), Times.Once); + } + + [Fact] + public void SequentialGoalStructure_WhenFirstGoalIsFinished_ShouldReturnUnfinished() + { + // Arrange + Mock> goalStructure1 = new(); + goalStructure1.SetupGet(g => g.Status).Returns(CompletionStatus.Success); + Mock> goalStructure2 = new(); + BeliefSet beliefSet = Mock.Of(); + SequentialGoalStructure sequentialGoalStructure = new(new List> + { + goalStructure1.Object, goalStructure2.Object + }); + + // Act + sequentialGoalStructure.UpdateStatus(beliefSet); + + // Assert + sequentialGoalStructure.Status.Should().Be(CompletionStatus.Unfinished); + } + + [Fact] + public void SequentialGoalStructure_WhenGoalFails_ShouldReturnFailure() + { + // Arrange + Mock> goalStructure1 = new(); + goalStructure1.SetupGet(g => g.Status).Returns(CompletionStatus.Failure); + Mock> goalStructure2 = new(); + BeliefSet beliefSet = Mock.Of(); + SequentialGoalStructure sequentialGoalStructure = new(new List> + { + goalStructure1.Object, goalStructure2.Object + }); + + // Act + sequentialGoalStructure.UpdateStatus(beliefSet); + + // Assert + sequentialGoalStructure.Status.Should().Be(CompletionStatus.Failure); + } + + [Fact] + public void SequentialGoalStructure_WhenProvidingNoGoalStructure_ShouldThrowException() + { + // Arrange + List> goalStructures = new(); + + // Act + Func> act = () => new SequentialGoalStructure(goalStructures); + + // Assert + act.Should().Throw(); + } +} diff --git a/Aplib.Tests/Desire/GoalTests.cs b/Aplib.Tests/Core/Desire/GoalTests.cs similarity index 75% rename from Aplib.Tests/Desire/GoalTests.cs rename to Aplib.Tests/Core/Desire/GoalTests.cs index 09522ed2..13503cc8 100644 --- a/Aplib.Tests/Desire/GoalTests.cs +++ b/Aplib.Tests/Core/Desire/GoalTests.cs @@ -1,15 +1,43 @@ +using Aplib.Core; using Aplib.Core.Belief; using Aplib.Core.Desire.Goals; using Aplib.Core.Intent.Tactics; -using Aplib.Tests.Stubs.Desire; using Aplib.Tests.Tools; using FluentAssertions; +using Moq; using Action = Aplib.Core.Intent.Actions.Action; -namespace Aplib.Tests.Desire; +namespace Aplib.Tests.Core.Desire; public class GoalTests { + /// + /// A test belief set that contains two public simple beliefs. + /// + private class MyBeliefSet : BeliefSet + { + /// + /// Belief that sets Updated to true when UpdateBelief is called. + /// + public readonly SimpleBelief MyBelief = new(); + } + + /// + /// A simple belief that can be used to test whether has been called. + /// + private class SimpleBelief : IBelief + { + /// + /// Stores whether has been called. + /// + public bool Updated { get; private set; } + + /// + /// Sets to true. + /// + public void UpdateBelief() => Updated = true; + } + /// /// Given valid parameters and metadata, /// When the goal is constructed, @@ -19,10 +47,11 @@ public class GoalTests public void Goal_WhenConstructed_ContainsCorrectMetaData() { // Arrange - Tactic tactic = new TacticStub(new Action(() => { })); + Tactic 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"; + 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 @@ -43,59 +72,60 @@ public void Goal_WhenConstructed_DidNotIterateYet() { // Arrange int iterations = 0; - Tactic tactic = new TacticStub(new Action(() => iterations++)); - + Mock tactic = new(); + tactic.Setup(x => x.GetAction()).Returns(new Action(() => { iterations++; })); // Act - Goal _ = new TestGoalBuilder().UseTactic(tactic).Build(); + Goal goal = new TestGoalBuilder().UseTactic(tactic.Object).Build(); // Assert + goal.Tactic.Should().Be(tactic.Object); iterations.Should().Be(0); } /// - /// Given the Goal's heuristic function is configured to have reached its goal + /// 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 true. + /// then the method should return false. /// [Fact] - public void Goal_WhenReached_ReturnsAsCompleted() + public void Goal_WhenNotReached_DoesNotReturnAsCompleted() { // Arrange MyBeliefSet beliefSet = new(); - Goal.HeuristicFunction heuristicFunction = CommonHeuristicFunctions.Completed(); + Goal.HeuristicFunction heuristicFunction = CommonHeuristicFunctions.Uncompleted(); // Act Goal goal = new TestGoalBuilder().WithHeuristicFunction(heuristicFunction).Build(); - bool isCompleted = goal.IsCompleted(beliefSet); + CompletionStatus isCompleted = goal.GetStatus(beliefSet); // Assert - isCompleted.Should().Be(true); + isCompleted.Should().Be(CompletionStatus.Unfinished); } /// - /// Given the Goal's heuristic function is configured to *not* have reached its goal, + /// 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 false. + /// then the method should return true. /// [Fact] - public void Goal_WhenNotReached_DoesNotReturnAsCompleted() + public void Goal_WhenReached_ReturnsAsCompleted() { // Arrange MyBeliefSet beliefSet = new(); - Goal.HeuristicFunction heuristicFunction = CommonHeuristicFunctions.Uncompleted(); + Goal.HeuristicFunction heuristicFunction = CommonHeuristicFunctions.Completed(); // Act Goal goal = new TestGoalBuilder().WithHeuristicFunction(heuristicFunction).Build(); - bool isCompleted = goal.IsCompleted(beliefSet); + CompletionStatus isCompleted = goal.GetStatus(beliefSet); // Assert - isCompleted.Should().Be(false); + isCompleted.Should().Be(CompletionStatus.Success); } /// /// Given a valid goal and belief, /// when the goal's heuristic function is evaluated, - /// the beliefset is not altered + /// the belief set is not altered /// [Fact] public void Goal_WhereEvaluationIsPerformed_DoesNotInfluenceBelieveSet() @@ -105,7 +135,7 @@ public void Goal_WhereEvaluationIsPerformed_DoesNotInfluenceBelieveSet() // Act Goal goal = new TestGoalBuilder().Build(); - _ = goal.IsCompleted(beliefSet); + _ = goal.GetStatus(beliefSet); // Assert beliefSet.MyBelief.Updated.Should().Be(false); @@ -123,9 +153,10 @@ public void Goal_WhereEvaluationIsPerformed_DoesNotInfluenceBelieveSet() public void GoalConstructor_WhereHeuristicFunctionTypeDiffers_HasEqualBehaviour(bool goalCompleted) { // Arrange - Tactic tactic = new TacticStub(new Action(() => { })); + Tactic tactic = Mock.Of(); 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"; + const string description = + "\"A lie is just a good story that someone ruined with the truth.\" - Barney Stinson"; Func heuristicFunctionBoolean = () => goalCompleted; Goal.HeuristicFunction heuristicFunctionNonBoolean = CommonHeuristicFunctions.Boolean(() => goalCompleted); @@ -135,40 +166,10 @@ public void GoalConstructor_WhereHeuristicFunctionTypeDiffers_HasEqualBehaviour( // Act MyBeliefSet beliefSet = new(); - bool goalBooleanEvaluation = goalBoolean.IsCompleted(beliefSet); - bool goalNonBooleanEvaluation = goalNonBoolean.IsCompleted(beliefSet); + CompletionStatus goalBooleanEvaluation = goalBoolean.GetStatus(beliefSet); + CompletionStatus goalNonBooleanEvaluation = goalNonBoolean.GetStatus(beliefSet); // Assert goalBooleanEvaluation.Should().Be(goalNonBooleanEvaluation); } - - /// - /// A test belief set that contains two public simple beliefs. - /// - private class MyBeliefSet : BeliefSet - { - /// - /// Belief that sets Updated to true when UpdateBelief is called. - /// - public SimpleBelief MyBelief = new(); - } - - /// - /// A simple belief that can be used to test whether has been called. - /// - private class SimpleBelief : IBelief - { - /// - /// Stores whether has been called. - /// - public bool Updated { get; private set; } = false; - - /// - /// Sets to true. - /// - public void UpdateBelief() - { - Updated = true; - } - } } diff --git a/Aplib.Tests/Core/ActionTests.cs b/Aplib.Tests/Core/Intent/Actions/ActionTests.cs similarity index 92% rename from Aplib.Tests/Core/ActionTests.cs rename to Aplib.Tests/Core/Intent/Actions/ActionTests.cs index 26b60b45..66bd83dd 100644 --- a/Aplib.Tests/Core/ActionTests.cs +++ b/Aplib.Tests/Core/Intent/Actions/ActionTests.cs @@ -1,10 +1,10 @@ using Aplib.Core.Intent.Actions; using Action = Aplib.Core.Intent.Actions.Action; -namespace Aplib.Tests.Core; +namespace Aplib.Tests.Core.Intent.Actions; /// -/// Describes a set of tests for the class. +/// Describes a set of tests for the class. /// public class ActionTests { @@ -18,7 +18,7 @@ public void Execute_SideEffects_ReturnsCorrectEffect() { // Arrange string? result = "abc"; - Action action = new(effect: () => result = "def"); + Action action = new(() => result = "def"); // Act action.Execute(); @@ -28,33 +28,35 @@ public void Execute_SideEffects_ReturnsCorrectEffect() } /// - /// Given an action with no query, - /// When checking if the action is actionable, - /// Then the result should always be true. + /// Given a guarded action with an int guard, + /// When the action is guarded and executed, + /// Then the result should be the value of the guard. /// [Fact] - public void IsActionable_NoQuery_AlwaysTrue() + public void Execute_WithGuard_ShouldInvokeQueryAndStoreResult() { // Arrange - Action action = new(effect: () => { }); + int result = 0; + GuardedAction action = new(guard: () => 42, effect: guard => result = guard); // Act - bool actionable = action.IsActionable(); + _ = action.IsActionable(); + action.Execute(); // Assert - Assert.True(actionable); + Assert.Equal(42, result); } /// - /// Given an action with a true query, + /// Given an action with no query, /// When checking if the action is actionable, - /// Then the result should be true. + /// Then the result should always be true. /// [Fact] - public void IsActionable_QueryWithTrue_ReturnsTrue() + public void IsActionable_NoQuery_AlwaysTrue() { // Arrange - Action action = new(effect: () => { }, guard: () => true); + Action action = new(() => { }); // Act bool actionable = action.IsActionable(); @@ -64,94 +66,92 @@ public void IsActionable_QueryWithTrue_ReturnsTrue() } /// - /// Given an action with a false query, + /// Given an action with a false bool guard, /// When checking if the action is actionable, - /// Then the result should be false. + /// Then the result should be true. /// [Fact] - public void IsActionable_QueryWithFalse_ReturnsFalse() + public void IsActionable_QueryIsFalse_IsActionable() { // Arrange - Action action = new(effect: () => { }, guard: () => false); + GuardedAction action = new(guard: () => false, effect: b => { }); // Act - bool actionable = action.IsActionable(); + bool result = action.IsActionable(); // Assert - Assert.False(actionable); + Assert.True(result); } /// - /// Given a guarded action with an int guard, - /// When the action is guarded and executed, - /// Then the result should be the value of the guard. + /// Given an action with a non-null int guard, + /// When checking if the action is actionable, + /// Then the result should be true. /// [Fact] - public void Execute_WithGuard_ShouldInvokeQueryAndStoreResult() + public void IsActionable_QueryIsNotNull_IsActionable() { // Arrange - int result = 0; - GuardedAction action = new(guard: () => 42, effect: (guard) => result = guard); + GuardedAction action = new(guard: () => 10, effect: b => { }); // Act - _ = action.IsActionable(); - action.Execute(); + bool result = action.IsActionable(); // Assert - Assert.Equal(42, result); + Assert.True(result); } /// - /// Given an action with a non-null int guard, + /// Given an action with a null object guard, /// When checking if the action is actionable, - /// Then the result should be true. + /// Then the result should be false. /// [Fact] - public void IsActionable_QueryIsNotNull_IsActionable() + public void IsActionable_QueryIsNull_IsNotActionable() { // Arrange - GuardedAction action = new(guard: () => 10, effect: b => { }); + GuardedAction action = new(guard: () => null!, effect: b => { }); // Act bool result = action.IsActionable(); // Assert - Assert.True(result); + Assert.False(result); } /// - /// Given an action with a false bool guard, + /// Given an action with a false query, /// When checking if the action is actionable, - /// Then the result should be true. + /// Then the result should be false. /// [Fact] - public void IsActionable_QueryIsFalse_IsActionable() + public void IsActionable_QueryWithFalse_ReturnsFalse() { // Arrange - GuardedAction action = new(guard: () => false, effect: b => { }); + Action action = new(() => { }, () => false); // Act - bool result = action.IsActionable(); + bool actionable = action.IsActionable(); // Assert - Assert.True(result); + Assert.False(actionable); } /// - /// Given an action with a null object guard, + /// Given an action with a true query, /// When checking if the action is actionable, - /// Then the result should be false. + /// Then the result should be true. /// [Fact] - public void IsActionable_QueryIsNull_IsNotActionable() + public void IsActionable_QueryWithTrue_ReturnsTrue() { // Arrange - GuardedAction action = new(guard: () => null!, effect: b => { }); + Action action = new(() => { }, () => true); // Act - bool result = action.IsActionable(); + bool actionable = action.IsActionable(); // Assert - Assert.False(result); + Assert.True(actionable); } } diff --git a/Aplib.Tests/Core/Tactics/TacticTests.cs b/Aplib.Tests/Core/Intent/Tactics/TacticTests.cs similarity index 87% rename from Aplib.Tests/Core/Tactics/TacticTests.cs rename to Aplib.Tests/Core/Intent/Tactics/TacticTests.cs index 181a179d..e62f8e7f 100644 --- a/Aplib.Tests/Core/Tactics/TacticTests.cs +++ b/Aplib.Tests/Core/Intent/Tactics/TacticTests.cs @@ -1,29 +1,44 @@ using Aplib.Core.Intent.Tactics; using Action = Aplib.Core.Intent.Actions.Action; -namespace Aplib.Tests.Core.Tactics; +namespace Aplib.Tests.Core.Intent.Tactics; + public class TacticTests { - private readonly Action _emptyAction = new(effect: () => { }); private static string _result = "abc"; - private readonly Action _filledAction = new(effect: () => _result = "def"); + private readonly Action _emptyAction = new(() => { }); + private readonly Action _filledAction = new(() => _result = "def"); - private static bool TrueGuard() => true; + /// + /// Given a tactic with a guard that returns true and an action, + /// When calling the Execute method, + /// Then _result should be "def". + /// + [Fact] + public void Execute_WhenGuardReturnsTrue_ActionIsExecuted() + { + // Arrange + PrimitiveTactic tactic = new(_filledAction, TrueGuard); - private static bool FalseGuard() => false; + // Act + tactic.GetAction()!.Execute(); + + // Assert + Assert.Equal("def", _result); + } /// - /// Given a parent of type with two subtactics, + /// Given a parent of type with two subtactics, /// When getting the next tactic, - /// Then the result should be the first subtactic. + /// Then the result should contain all the subtactics. /// [Fact] - public void GetAction_WhenTacticTypeIsFirstOf_ReturnsEnabledPrimitiveTactics() + public void GetAction_WhenTacticTypeIsAnyOf_ReturnsEnabledPrimitiveTactics() { // Arrange PrimitiveTactic tactic1 = new(_emptyAction); - PrimitiveTactic tactic2 = new(_filledAction); - FirstOfTactic parentTactic = new([tactic1, tactic2]); + PrimitiveTactic tactic2 = new(_emptyAction); + AnyOfTactic parentTactic = new(tactic1, tactic2); // Act Action? enabledAction = parentTactic.GetAction(); @@ -34,17 +49,17 @@ public void GetAction_WhenTacticTypeIsFirstOf_ReturnsEnabledPrimitiveTactics() } /// - /// Given a parent of type with two subtactics and a guard that is true, + /// Given a parent of type with two subtactics, /// When getting the next tactic, /// Then the result should be the first subtactic. /// [Fact] - public void GetAction_WhenTacticTypeIsFirstOfAndGuardEnabled_ReturnsEnabledPrimitiveTactics() + public void GetAction_WhenTacticTypeIsFirstOf_ReturnsEnabledPrimitiveTactics() { // Arrange PrimitiveTactic tactic1 = new(_emptyAction); PrimitiveTactic tactic2 = new(_filledAction); - FirstOfTactic parentTactic = new(TrueGuard, [tactic1, tactic2]); + FirstOfTactic parentTactic = new(tactic1, tactic2); // Act Action? enabledAction = parentTactic.GetAction(); @@ -55,17 +70,17 @@ public void GetAction_WhenTacticTypeIsFirstOfAndGuardEnabled_ReturnsEnabledPrimi } /// - /// Given a parent of type with two subtactics, + /// Given a parent of type with two subtactics and a guard that is true, /// When getting the next tactic, - /// Then the result should contain all the subtactics. + /// Then the result should be the first subtactic. /// [Fact] - public void GetAction_WhenTacticTypeIsAnyOf_ReturnsEnabledPrimitiveTactics() + public void GetAction_WhenTacticTypeIsFirstOfAndGuardEnabled_ReturnsEnabledPrimitiveTactics() { // Arrange PrimitiveTactic tactic1 = new(_emptyAction); - PrimitiveTactic tactic2 = new(_emptyAction); - AnyOfTactic parentTactic = new([tactic1, tactic2]); + PrimitiveTactic tactic2 = new(_filledAction); + FirstOfTactic parentTactic = new(TrueGuard, tactic1, tactic2); // Act Action? enabledAction = parentTactic.GetAction(); @@ -113,21 +128,21 @@ public void GetAction_WhenTacticTypeIsPrimitiveAndActionIsNotActionable_ReturnsE } /// - /// Given a tactic with a guard that returns true and an action, - /// When calling the Execute method, - /// Then _result should be "def". + /// Given a tactic with a guard that returns false, + /// When checking if the tactic is actionable, + /// Then the result should be false. /// [Fact] - public void Execute_WhenGuardReturnsTrue_ActionIsExecuted() + public void IsActionable_WhenGuardReturnsFalse_ReturnsFalse() { // Arrange - PrimitiveTactic tactic = new(_filledAction, TrueGuard); + PrimitiveTactic tactic = new(_emptyAction, FalseGuard); // Act - tactic.GetAction()!.Execute(); + bool isActionable = tactic.IsActionable(); // Assert - Assert.Equal("def", _result); + Assert.False(isActionable); } /// @@ -148,21 +163,7 @@ public void IsActionable_WhenGuardReturnsTrue_ReturnsTrue() Assert.True(isActionable); } - /// - /// Given a tactic with a guard that returns false, - /// When checking if the tactic is actionable, - /// Then the result should be false. - /// - [Fact] - public void IsActionable_WhenGuardReturnsFalse_ReturnsFalse() - { - // Arrange - PrimitiveTactic tactic = new(_emptyAction, FalseGuard); - - // Act - bool isActionable = tactic.IsActionable(); + private static bool FalseGuard() => false; - // Assert - Assert.False(isActionable); - } + private static bool TrueGuard() => true; } diff --git a/Aplib.Tests/Stubs/Desire/TacticStub.cs b/Aplib.Tests/Stubs/Desire/TacticStub.cs deleted file mode 100644 index 5843fc4e..00000000 --- a/Aplib.Tests/Stubs/Desire/TacticStub.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Aplib.Core.Intent.Tactics; -using Action = Aplib.Core.Intent.Actions.Action; - -namespace Aplib.Tests.Stubs.Desire; - -/// -/// A fake tactic, which is just a wrapper around the you define as argument. -/// -/// The method to be executed during iteration. -internal class TacticStub(Action iteration) : Tactic -{ - /// - public override Action? GetAction() => iteration; -} diff --git a/Aplib.Tests/Tools/TestGoalBuilder.cs b/Aplib.Tests/Tools/TestGoalBuilder.cs index 8d6d0574..9cf586b2 100644 --- a/Aplib.Tests/Tools/TestGoalBuilder.cs +++ b/Aplib.Tests/Tools/TestGoalBuilder.cs @@ -1,24 +1,22 @@ using Aplib.Core.Desire.Goals; using Aplib.Core.Intent.Tactics; -using Aplib.Tests.Stubs.Desire; -using Action = Aplib.Core.Intent.Actions.Action; +using Moq; namespace Aplib.Tests.Tools; internal sealed class TestGoalBuilder { - private Tactic _tactic = new TacticStub(new Action(() => { })); + 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 string _description = "\"A lie is just a good story that someone ruined with the truth.\" ~ Barney Stinson"; - - public TestGoalBuilder() { } + private Tactic _tactic = Mock.Of(); public TestGoalBuilder WithHeuristicFunction(Goal.HeuristicFunction heuristicFunction) { _heuristicFunction = heuristicFunction; return this; } + public TestGoalBuilder WithHeuristicFunction(Func heuristicFunction) => WithHeuristicFunction(CommonHeuristicFunctions.Boolean(heuristicFunction));