diff --git a/.editorconfig b/.editorconfig index 62cd303a..7bfb4b9b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -195,13 +195,9 @@ 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 +dotnet_naming_rule.interface_should_begin_with_i.severity = warning +dotnet_naming_rule.interface_should_begin_with_i.symbols = interface +dotnet_naming_rule.interface_should_begin_with_i.style = begin_with_i dotnet_naming_rule.types_should_be_pascal_case.severity = warning dotnet_naming_rule.types_should_be_pascal_case.symbols = types @@ -219,47 +215,36 @@ 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 +dotnet_naming_rule.non_public_fields_with_underscore.severity = warning +dotnet_naming_rule.non_public_fields_with_underscore.symbols = non_public_fields +dotnet_naming_rule.non_public_fields_with_underscore.style = prefix_underscore + # 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.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.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.public_fields.applicable_kinds = field, property -dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal, protected_internal +dotnet_naming_symbols.public_fields.applicable_kinds = field +dotnet_naming_symbols.public_fields.applicable_accessibilities = public -dotnet_naming_symbols.private_fields.applicable_kinds = field, property -dotnet_naming_symbols.private_fields.applicable_accessibilities = protected, private_protected, private +dotnet_naming_symbols.non_public_fields.applicable_kinds = field +dotnet_naming_symbols.non_public_fields.applicable_accessibilities = private, protected, private_protected, internal, protected_internal # 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.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.capitalization = pascal_case +dotnet_naming_style.begin_with_i.required_prefix = I +dotnet_naming_style.begin_with_i.capitalization = pascal_case -dotnet_naming_style.prefix_underscore.capitalization = camel_case dotnet_naming_style.prefix_underscore.required_prefix = _ - -# Use underscores for private fields -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 - +dotnet_naming_style.prefix_underscore.capitalization = camel_case diff --git a/.gitignore b/.gitignore index e836e82b..59dd0669 100644 --- a/.gitignore +++ b/.gitignore @@ -446,3 +446,6 @@ fabric.properties *.iml modules.xml + +# JetBrains IDE files +**/.idea/ \ No newline at end of file diff --git a/Aplib.Tests/Aplib.Core.Tests.csproj b/Aplib.Core.Tests/Aplib.Core.Tests.csproj similarity index 95% rename from Aplib.Tests/Aplib.Core.Tests.csproj rename to Aplib.Core.Tests/Aplib.Core.Tests.csproj index 2182b62a..5f1377d0 100644 --- a/Aplib.Tests/Aplib.Core.Tests.csproj +++ b/Aplib.Core.Tests/Aplib.Core.Tests.csproj @@ -35,8 +35,4 @@ - - - - diff --git a/Aplib.Tests/BdiAgentTests.cs b/Aplib.Core.Tests/BdiAgentTests.cs similarity index 97% rename from Aplib.Tests/BdiAgentTests.cs rename to Aplib.Core.Tests/BdiAgentTests.cs index 783214b9..3745714a 100644 --- a/Aplib.Tests/BdiAgentTests.cs +++ b/Aplib.Core.Tests/BdiAgentTests.cs @@ -1,5 +1,6 @@ -using Aplib.Core.Belief; -using Aplib.Core.Desire; +using Aplib.Core.Agents; +using Aplib.Core.Belief.BeliefSets; +using Aplib.Core.Desire.DesireSets; using Aplib.Core.Desire.Goals; using Aplib.Core.Intent.Actions; using Aplib.Core.Intent.Tactics; diff --git a/Aplib.Tests/Belief/BeliefSetTests.cs b/Aplib.Core.Tests/Belief/BeliefSetTests.cs similarity index 98% rename from Aplib.Tests/Belief/BeliefSetTests.cs rename to Aplib.Core.Tests/Belief/BeliefSetTests.cs index f4c54e10..6a0b2a4f 100644 --- a/Aplib.Tests/Belief/BeliefSetTests.cs +++ b/Aplib.Core.Tests/Belief/BeliefSetTests.cs @@ -1,4 +1,5 @@ -using Aplib.Core.Belief; +using Aplib.Core.Belief.Beliefs; +using Aplib.Core.Belief.BeliefSets; namespace Aplib.Core.Tests.Belief; @@ -98,7 +99,6 @@ private class TestBeliefSetProperties : BeliefSet /// Belief that sets Updated to true when UpdateBelief is called. /// public SimpleBelief Belief2 { get; } = new(); - } @@ -122,7 +122,6 @@ private class TestBeliefSetPrivate : BeliefSet /// public SimpleBelief Belief2 => _belief2; - } /// diff --git a/Aplib.Core.Tests/Belief/BeliefTests.cs b/Aplib.Core.Tests/Belief/BeliefTests.cs new file mode 100644 index 00000000..9c8e7237 --- /dev/null +++ b/Aplib.Core.Tests/Belief/BeliefTests.cs @@ -0,0 +1,269 @@ +using Aplib.Core.Belief.Beliefs; +using FluentAssertions; +using Moq; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Aplib.Core.Tests.Belief; + +/// +/// Describes a set of tests for the class. +/// +public class BeliefTests +{ + // For testing a C# bug, see `Belief_ConstructedWithAValueTypeViaAnInterface_IsRejected` + private struct MyEnumerable : IEnumerable + { + private readonly int _number; + + private const int _max = 3; + + public MyEnumerable(int number) => _number = number; + + public IEnumerator GetEnumerator() + { + for (int i = 0; i < _max; i++) + { + yield return _number; + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + public class TestBelief : Belief + { + public object Reference => _reference; + + public Func GetObservationFromReference => _getObservationFromReference; + + public Func ShouldUpdate => _shouldUpdate; + + public TestBelief + ( + Metadata metadata, + object reference, + Func getObservationFromReference, + Func shouldUpdate + ) + : base(metadata, reference, getObservationFromReference, shouldUpdate) + { + } + + public TestBelief(object reference, Func getObservationFromReference, Func shouldUpdate) + : base(reference, getObservationFromReference, shouldUpdate) + { + } + + public TestBelief(Metadata metadata, object reference, Func getObservationFromReference) + : base(metadata, reference, getObservationFromReference) + { + } + + public TestBelief(object reference, Func getObservationFromReference) + : base(reference, getObservationFromReference) + { + } + } + + [Fact] + public void Belief_WhenConstructed_HasExpectedData() + { + // Arrange + Metadata metadata = It.IsAny(); + object reference = new Mock().Object; + Func getObservationFromReference = new Mock>().Object; + Func shouldUpdate = It.IsAny>(); + + // Act + TestBelief belief = new(metadata, reference, getObservationFromReference, shouldUpdate); + + // Assert + belief.Metadata.Should().Be(metadata); + belief.Reference.Should().Be(reference); + belief.GetObservationFromReference.Should().Be(getObservationFromReference); + ((object)belief.ShouldUpdate).Should().Be(shouldUpdate); + } + + [Fact] + public void Belief_WithoutMetadata_HasExpectedData() + { + // Arrange + object reference = new Mock().Object; + Func getObservationFromReference = new Mock>().Object; + Func shouldUpdate = It.IsAny>(); + + // Act + TestBelief belief = new(reference, getObservationFromReference, shouldUpdate); + + // Assert + belief.Metadata.Id.Should().NotBeEmpty(); + belief.Metadata.Name.Should().BeNull(); + belief.Metadata.Description.Should().BeNull(); + belief.Reference.Should().Be(reference); + belief.GetObservationFromReference.Should().Be(getObservationFromReference); + ((object)belief.ShouldUpdate).Should().Be(shouldUpdate); + } + + [Fact] + public void Belief_WithoutShouldUpdate_HasExpectedData() + { + // Arrange + Metadata metadata = It.IsAny(); + object reference = new Mock().Object; + Func getObservationFromReference = new Mock>().Object; + + // Act + TestBelief belief = new(metadata, reference, getObservationFromReference); + + // Assert + belief.Metadata.Should().Be(metadata); + belief.Reference.Should().Be(reference); + belief.GetObservationFromReference.Should().Be(getObservationFromReference); + belief.ShouldUpdate().Should().BeTrue(); + } + + [Fact] + public void Belief_WithoutMetadataWithoutShouldUpdate_HasExpectedData() + { + // Arrange + object reference = new Mock().Object; + Func getObservationFromReference = new Mock>().Object; + + // Act + TestBelief belief = new(reference, getObservationFromReference); + + // Assert + belief.Metadata.Id.Should().NotBeEmpty(); + belief.Metadata.Name.Should().BeNull(); + belief.Metadata.Description.Should().BeNull(); + belief.Reference.Should().Be(reference); + belief.GetObservationFromReference.Should().Be(getObservationFromReference); + belief.ShouldUpdate().Should().BeTrue(); + } + + /// + /// Given a Belief instance, + /// When it is assigned to a variable of its observation type, + /// Then it is implicitly converted to its observation type. + /// + [Fact] + public void Belief_AssignedToObservationType_IsCorrectlyImplicitlyConvertedToObservationType() + { + // Arrange + // ReSharper disable once ConvertToConstant.Local + string def = "def"; + + // Observation: Get the first letter. + Belief belief = new(def, reference => reference[0]); + + // Act, Assert + Assert.Equal(belief.Observation, belief); + } + + /// + /// Given a reference that is actually a value type, hidden behind an interface, + /// When a new Belief is constructed from this reference, + /// The constructor throws an ArgumentException. + /// + [Fact] + public void Belief_ConstructedWithAValueTypeViaAnInterface_IsRejected() + { + // Arrange + MyEnumerable value = new(1); + const string paramName = "reference"; + + // ReSharper disable once ConvertToLocalFunction + Action construction = () => + { + // The bug is the fact that we can get around the constraint that `TReference` should be a reference type. + _ = new Belief, List>(value, values => values.ToList()); + }; + + // Act, Assert + Assert.Throws(paramName, construction); + } + + /// + /// Given a reference, + /// When a new Belief is constructed, + /// Then the observation is also initialized. + /// + [Fact] + public void Belief_DuringConstruction_UpdatesTheObservation() + { + // Arrange + // ReSharper disable once ConvertToConstant.Local + string def = "def"; + + // Act + Belief belief = new(def, str => str); + + // Assert + Assert.Equal(def, belief.Observation); + } + + /// + /// Given a Belief instance with a reference, + /// When the reference is assigned to and UpdateBelief is called, + /// Then the observation is not updated. + /// + [Fact] + public void UpdateBelief_ReferenceIsAssignedTo_DoesNotUpdateObservation() + { + // Arrange + string def = "def"; + Belief belief = new(def, reference => reference, () => true); + + // Act + def = "abc"; + belief.UpdateBelief(); + + // Assert + Assert.NotEqual(def, belief.Observation); + } + + /// + /// Given a Belief instance with an shouldUpdate condition that is not satisfied, + /// When UpdateBelief is called, + /// Then the observation is not updated. + /// + [Fact] + public void UpdateBelief_ShouldUpdateConditionIsNotSatisfied_DoesNotUpdateObservation() + { + // Arrange + List list = []; + Belief, int> belief = new(list, reference => reference.Count, () => false); + + // Act + list.Add(420); + belief.UpdateBelief(); + + // Assert + Assert.NotEqual(list.Count, belief); + Assert.NotEqual(list.Count, belief.Observation); + } + + /// + /// Given a Belief instance with an shouldUpdate condition that is satisfied, + /// When UpdateBelief is called, + /// Then the observation is updated. + /// + [Fact] + public void UpdateBelief_ShouldUpdateConditionIsSatisfied_UpdatesObservation() + { + // Arrange + List list = []; + Belief, int> belief = new(list, reference => reference.Count, () => true); + + // Act + list.Add(69); + belief.UpdateBelief(); + + // Assert + Assert.Equal(list.Count, belief); + Assert.Equal(list.Count, belief.Observation); + } +} diff --git a/Aplib.Core.Tests/Belief/ListBeliefTests.cs b/Aplib.Core.Tests/Belief/ListBeliefTests.cs new file mode 100644 index 00000000..c509baf0 --- /dev/null +++ b/Aplib.Core.Tests/Belief/ListBeliefTests.cs @@ -0,0 +1,75 @@ +using Aplib.Core.Belief.Beliefs; +using System.Collections.Generic; + +namespace Aplib.Core.Tests.Belief; + +/// +/// Unit tests for . +/// +public class ListBeliefTests +{ + /// + /// Given a ListBelief without an explicit shouldUpdate parameter, + /// When UpdateBelief is called, + /// Then the observation is updated. + /// + [Fact] + public void ListBelief_WithoutShouldUpdate_UpdatesObservation() + { + // Arrange + int[] numbers = [1, 1, 2, 3, 5, 8]; + + // Observation: Is the number even? + ListBelief belief = new(numbers, i => i % 2 == 0); + + // Act + numbers[0] = 0; + belief.UpdateBelief(); + + // Assert + List expected = [true, false, true, false, false, true]; + Assert.Equal(expected, belief); + } + + /// + /// Given a ListBelief with a shouldUpdate condition that is not satisfied, + /// When UpdateBelief is called, + /// Then the observation is not updated. + /// + [Fact] + public void ListBelief_ShouldUpdateConditionIsNotSatisfied_DoesNotUpdateObservation() + { + // Arrange + List strings = ["foo", "bar"]; + + // Observation: What is the last character? + ListBelief belief = new(strings, str => str[^1], () => false); + + // Act + // Append 'x' to each string + for (int i = 0; i < strings.Count; i++) strings[i] += 'x'; + belief.UpdateBelief(); + + // Assert + Assert.Equal(new List { 'o', 'r' }, belief); + } + + /// + /// Given an empty collection, + /// When a ListBelief is created from that collection, + /// Then the observation is also empty + /// + [Fact] + public void ListBelief_FromEmptyEnumerable_HasEmptyObservationList() + { + // Arrange + // ReSharper disable once CollectionNeverUpdated.Local + Stack stack = new(); + + // Act + ListBelief belief = new(stack, b => b); + + // Assert + Assert.Empty(belief.Observation); + } +} diff --git a/Aplib.Core.Tests/Belief/MemoryBeliefTests.cs b/Aplib.Core.Tests/Belief/MemoryBeliefTests.cs new file mode 100644 index 00000000..eebac15b --- /dev/null +++ b/Aplib.Core.Tests/Belief/MemoryBeliefTests.cs @@ -0,0 +1,233 @@ +using Aplib.Core.Belief.Beliefs; +using Aplib.Core.Collections; +using FluentAssertions; +using Moq; +using System; +using System.Collections.Generic; + +namespace Aplib.Core.Tests.Belief; + +/// +/// Describes a set of tests for the class. +/// +public class MemoryBeliefTests +{ + public class TestMemoryBelief : MemoryBelief + { + public object Reference => _reference; + + public Func GetObservationFromReference => _getObservationFromReference; + + public Func ShouldUpdate => _shouldUpdate; + + public ExposedQueue MemorizedObservations => _memorizedObservations; + + public TestMemoryBelief + ( + Metadata metadata, + object reference, + Func getObservationFromReference, + int framesToRemember, + Func shouldUpdate + ) + : base(metadata, reference, getObservationFromReference, framesToRemember, shouldUpdate) + { + } + + public TestMemoryBelief + ( + object reference, + Func getObservationFromReference, + int framesToRemember, + Func shouldUpdate + ) + : base(reference, getObservationFromReference, framesToRemember, shouldUpdate) + { + } + + public TestMemoryBelief + ( + Metadata metadata, + object reference, + Func getObservationFromReference, + int framesToRemember + ) + : base(metadata, reference, getObservationFromReference, framesToRemember) + { + } + + public TestMemoryBelief + (object reference, Func getObservationFromReference, int framesToRemember) + : base(reference, getObservationFromReference, framesToRemember) + { + } + } + + [Fact] + public void MemoryBelief_WhenConstructed_HasExpectedData() + { + // Arrange + Metadata metadata = It.IsAny(); + object reference = new Mock().Object; + Func getObservationFromReference = new Mock>().Object; + const int framesToRemember = 0; + Func shouldUpdate = It.IsAny>(); + + // Act + TestMemoryBelief belief = new(metadata, reference, getObservationFromReference, framesToRemember, shouldUpdate); + + // Assert + belief.Metadata.Should().Be(metadata); + belief.Reference.Should().Be(reference); + belief.GetObservationFromReference.Should().Be(getObservationFromReference); + belief.MemorizedObservations.MaxCount.Should().Be(framesToRemember); + ((object)belief.ShouldUpdate).Should().Be(shouldUpdate); + } + + [Fact] + public void MemoryBelief_WithoutMetadata_HasExpectedData() + { + // Arrange + object reference = new Mock().Object; + Func getObservationFromReference = new Mock>().Object; + const int framesToRemember = 1; + Func shouldUpdate = It.IsAny>(); + + // Act + TestMemoryBelief belief = new(reference, getObservationFromReference, framesToRemember, shouldUpdate); + + // Assert + belief.Metadata.Id.Should().NotBeEmpty(); + belief.Metadata.Name.Should().BeNull(); + belief.Metadata.Description.Should().BeNull(); + belief.Reference.Should().Be(reference); + belief.GetObservationFromReference.Should().Be(getObservationFromReference); + belief.MemorizedObservations.MaxCount.Should().Be(framesToRemember); + ((object)belief.ShouldUpdate).Should().Be(shouldUpdate); + } + + [Fact] + public void MemoryBelief_WithoutShouldUpdate_HasExpectedData() + { + // Arrange + Metadata metadata = It.IsAny(); + object reference = new Mock().Object; + Func getObservationFromReference = new Mock>().Object; + const int framesToRemember = 2; + + // Act + TestMemoryBelief belief = new(metadata, reference, getObservationFromReference, framesToRemember); + + // Assert + belief.Metadata.Should().Be(metadata); + belief.Reference.Should().Be(reference); + belief.GetObservationFromReference.Should().Be(getObservationFromReference); + belief.MemorizedObservations.MaxCount.Should().Be(framesToRemember); + belief.ShouldUpdate().Should().BeTrue(); + } + + [Fact] + public void MemoryBelief_WithoutMetadataWithoutShouldUpdate_HasExpectedData() + { + // Arrange + object reference = new Mock().Object; + Func getObservationFromReference = new Mock>().Object; + const int framesToRemember = 3; + + // Act + TestMemoryBelief belief = new(reference, getObservationFromReference, framesToRemember); + + // Assert + belief.Metadata.Id.Should().NotBeEmpty(); + belief.Metadata.Name.Should().BeNull(); + belief.Metadata.Description.Should().BeNull(); + belief.Reference.Should().Be(reference); + belief.GetObservationFromReference.Should().Be(getObservationFromReference); + belief.MemorizedObservations.MaxCount.Should().Be(framesToRemember); + belief.ShouldUpdate().Should().BeTrue(); + } + + /// + /// Given a MemoryBelief instance with an observation, + /// When the observation is updated and GetMostRecentMemory is called, + /// Then the last observation is returned. + /// + [Fact] + public void GetMostRecentMemory_WhenObservationIsUpdated_ShouldReturnLastObservation() + { + // Arrange + List list = [1]; + MemoryBelief, int> belief = new(list, reference => reference.Count, 1); + + // Act + list.Add(2); + belief.UpdateBelief(); + + // Assert + Assert.Equal(1, belief.GetMostRecentMemory()); + } + + /// + /// Given a MemoryBelief instance with an observation, + /// When the observation is updated and GetMemoryAt is called with an index, + /// Then the observation at the specified index is returned. + /// + [Fact] + public void GetMemoryAt_WhenObservationIsUpdated_ShouldReturnObservationAtSpecifiedIndex() + { + // Arrange + List list = [1, 2, 3]; + MemoryBelief, int> belief = new(list, reference => reference.Count, 3); + + // Act + list.Add(4); + belief.UpdateBelief(); + list.Add(5); + belief.UpdateBelief(); + + // Assert + Assert.Equal(4, belief.GetMemoryAt(0)); + Assert.Equal(3, belief.GetMemoryAt(1)); + } + + /// + /// Given a MemoryBelief instance with an observation, + /// When asking for an index that is out of bounds, + /// Then an exception should be thrown. + /// + [Fact] + public void GetMemoryAt_IndexOutOfBounds_ShouldThrowException() + { + // Arrange + List list = [1, 2, 3]; + MemoryBelief, int> belief = new(list, reference => reference.Count, 3); + + // Act + void GetMemoryAtNegativeIndex() => belief.GetMemoryAt(-1); + void GetMemoryAtIndexGreaterThanCount() => belief.GetMemoryAt(3); + + // Assert + Assert.Throws(GetMemoryAtNegativeIndex); + Assert.Throws(GetMemoryAtIndexGreaterThanCount); + } + + /// + /// Given a MemoryBelief instance with an observation, + /// When the observation is updated and GetAllMemories is called, + /// Then all the currently saved observations are returned. + /// + [Fact] + public void GetAllMemories_ReturnsAllMemories() + { + // Arrange + List list = [1, 2, 3]; + MemoryBelief, int> belief = new(list, reference => reference.Count, 3); + + // Act + list.Add(4); + belief.UpdateBelief(); + + // Assert + Assert.Equal([3], belief.GetAllMemories()); + } +} diff --git a/Aplib.Core.Tests/Belief/SampledMemoryBeliefTests.cs b/Aplib.Core.Tests/Belief/SampledMemoryBeliefTests.cs new file mode 100644 index 00000000..6b67835b --- /dev/null +++ b/Aplib.Core.Tests/Belief/SampledMemoryBeliefTests.cs @@ -0,0 +1,235 @@ +using Aplib.Core.Belief.Beliefs; +using System.Collections.Generic; +using static Aplib.Core.Belief.Beliefs.UpdateMode; + +namespace Aplib.Core.Tests.Belief; + +/// +/// Describes a set of tests for the class. +/// +public class SampledMemoryBeliefTests +{ + /// + /// Given a SampledMemoryBelief instance with update mode 'AlwaysUpdate', + /// When the belief is not in a sampleInterval-th cycle, + /// Then GetMostRecentMemory should be outdated. + /// + [Fact] + public void GetMostRecentMemory_WhenUpdateModeIsAlwaysUpdate_ShouldBeOutdated() + { + // Arrange + List list = []; + int sampleInterval = 2, + framesToRemember = 3; + SampledMemoryBelief, int> belief + = new(list, reference => reference.Count, sampleInterval, AlwaysUpdate, framesToRemember); + + // Act + // Expected values: + // ---------------------------------------------------- + // | list.Count | Observation | Memory + // ---------------------------------------------------- + // Initial | 0 | 0 * | [0, 0, 0] + // Iteration 0 | 1 | 1 | [0, 0, 0] (memory * is sampled) + // Iteration 1 | 2 | 2 * | [0, 0, 0] + // Iteration 2 | 3 | 3 | [2, 0, 0] (memory * is sampled) + // Iteration 3 | 4 | 4 * | [2, 0, 0] + // Iteration 4 | 5 | 5 | [4, 2, 0] (memory * is sampled) + // Iteration 5 | 6 | 6 | [4, 2, 0] + // ---------------------------------------------------- + for (int i = 0; i < 6; i++) + { + list.Add(0); + belief.UpdateBelief(); + } + + // Assert + Assert.Equal(4, belief.GetMostRecentMemory()); + } + + /// + /// Given a SampledMemoryBelief instance with update mode 'UpdateWhenSampled', + /// When the belief is not in a sampleInterval-th cycle, + /// Then GetMostRecentMemory should be outdated. + /// + [Fact] + public void GetMostRecentMemory_WhenUpdateModeIsUpdateWhenSampled_ShouldBeOutdated() + { + // Arrange + List list = []; + int sampleInterval = 2, + framesToRemember = 3; + SampledMemoryBelief, int> belief + = new(list, reference => reference.Count, sampleInterval, UpdateWhenSampled, framesToRemember); + + // Act + // Expected values: + // ---------------------------------------------------- + // | list.Count | Observation | Memory + // ---------------------------------------------------- + // Initial | 0 | 0 * | [0, 0, 0] + // Iteration 0 | 1 | 1 | [0, 0, 0] (memory * is sampled & observation is updated) + // Iteration 1 | 2 | 1 * | [0, 0, 0] + // Iteration 2 | 3 | 3 | [1, 0, 0] (memory * is sampled & observation is updated) + // Iteration 3 | 4 | 3 * | [1, 0, 0] + // Iteration 4 | 5 | 5 | [3, 1, 0] (memory * is sampled & observation is updated) + // Iteration 5 | 6 | 5 | [3, 1, 0] + // ---------------------------------------------------- + for (int i = 0; i < 6; i++) + { + list.Add(0); + belief.UpdateBelief(); + } + + // Assert + Assert.Equal(3, belief.GetMostRecentMemory()); + } + + /// + /// Given a SampledMemoryBelief instance with update mode 'AlwaysUpdate', + /// When the belief is not in a sampleInterval-th cycle, + /// Then Observation should be up-to-date. + /// + [Fact] + public void Observation_WhenUpdateModeIsAlwaysUpdate_ShouldBeUpToDate() + { + // Arrange + List list = []; + int sampleInterval = 2, + framesToRemember = 3; + SampledMemoryBelief, int> belief + = new(list, reference => reference.Count, sampleInterval, AlwaysUpdate, framesToRemember); + + // Act + // Expected values: + // ----------------------------------------- + // | list.Count | Observation + // ----------------------------------------- + // Initial | 0 | 0 (observation is updated) + // Iteration 0 | 1 | 1 (observation is updated) + // Iteration 1 | 2 | 2 (observation is updated) + // ----------------------------------------- + for (int i = 0; i < 2; i++) + { + list.Add(0); + belief.UpdateBelief(); + } + + // Assert + Assert.Equal(list.Count, belief.Observation); + } + + /// + /// Given a SampledMemoryBelief instance with update mode 'UpdateWhenSampled', + /// When the belief is not in a sampleInterval-th cycle, + /// Then Observation should be outdated. + /// + [Fact] + public void Observation_WhenUpdateModeIsUpdateWhenSampled_ShouldBeOutdated() + { + // Arrange + List list = []; + int sampleInterval = 2, + framesToRemember = 3; + SampledMemoryBelief, int> belief + = new(list, reference => reference.Count, sampleInterval, UpdateWhenSampled, framesToRemember); + + // Act + // Expected values: + // ----------------------------------------- + // | list.Count | Observation + // ----------------------------------------- + // Initial | 0 | 0 + // Iteration 0 | 1 | 1 (observation is updated) + // Iteration 1 | 2 | 1 + // Iteration 2 | 3 | 3 (observation is updated) + // Iteration 3 | 4 | 3 + // ----------------------------------------- + for (int i = 0; i < 4; i++) + { + list.Add(0); + belief.UpdateBelief(); + } + + // Assert + Assert.NotEqual(list.Count, belief.Observation); + } + + /// + /// Given a SampledMemoryBelief instance with update mode 'UpdateWhenSampled', + /// When the belief is not in a sampleInterval-th cycle, + /// Then Observation should have a value from the previous sample interval. + /// + [Theory] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + public void Observation_WhenUpdateModeIsUpdateWhenSampled_ShouldBeFromPreviousSampleInterval(int sampleInterval) + { + // Arrange + List list = []; + int framesToRemember = 3; + SampledMemoryBelief, int> belief + = new(list, reference => reference.Count, sampleInterval, UpdateWhenSampled, framesToRemember); + + // Act + // Expected values: + // ------------------------------------------------------------------ + // | list.Count | Observation + // ------------------------------------------------------------------ + // Initial | 0 | 0 + // Iteration 0 | 1 | 1 (observation is updated) + // Iteration 1 | 2 | 1 + // ... + // Iteration sampleInterval | sampleInterval + 1 | sampleInterval + 1 (observation is updated) + // ... + // ------------------------------------------------------------------ + // Iterate 2 intervals. + for (int i = 0; i < sampleInterval * 2; i++) + { + list.Add(0); + belief.UpdateBelief(); + } + + // Assert + // We expect the previous observation update to occur at the sampleInterval-th iteration/cycle, + // when the list count is sampleInterval + 1 (see table). + int expected = sampleInterval + 1; + Assert.Equal(expected, belief.Observation); + } + + /// + /// Given a SampledMemoryBelief instance with a shouldUpdate method, + /// When shouldUpdate returns false, + /// Then Observation should not be updated regardless of the update mode. + /// + [Fact] + public void Observation_WhenShouldUpdateReturnsFalse_ShouldNotBeUpdated() + { + // Arrange + List list = []; + int sampleInterval = 2, + framesToRemember = 3; + SampledMemoryBelief, int> belief + = new(list, reference => reference.Count, sampleInterval, AlwaysUpdate, framesToRemember, () => false); + + // Act + // Expected values: + // ----------------------------------------- + // | list.Count | Observation + // ----------------------------------------- + // Initial | 0 | 0 (set initial observation) + // Iteration 0 | 1 | 0 + // Iteration 1 | 2 | 0 + // ----------------------------------------- + for (int i = 0; i < 2; i++) + { + list.Add(0); + belief.UpdateBelief(); + } + + // Assert + Assert.Equal(0, belief.Observation); + } +} diff --git a/Aplib.Tests/Belief/CircularArrayTests.cs b/Aplib.Core.Tests/Collections/CircularArrayTests.cs similarity index 97% rename from Aplib.Tests/Belief/CircularArrayTests.cs rename to Aplib.Core.Tests/Collections/CircularArrayTests.cs index af5fc52f..34cda7ed 100644 --- a/Aplib.Tests/Belief/CircularArrayTests.cs +++ b/Aplib.Core.Tests/Collections/CircularArrayTests.cs @@ -1,4 +1,6 @@ -namespace Aplib.Core.Tests.Belief; +using Aplib.Core.Collections; + +namespace Aplib.Core.Tests.Collections; public class CircularArrayTests { diff --git a/Aplib.Core.Tests/Collections/ExposedQueueTests.cs b/Aplib.Core.Tests/Collections/ExposedQueueTests.cs new file mode 100644 index 00000000..25b35aac --- /dev/null +++ b/Aplib.Core.Tests/Collections/ExposedQueueTests.cs @@ -0,0 +1,316 @@ +using Aplib.Core.Collections; +using System; +using System.Collections.Generic; + +namespace Aplib.Core.Tests.Collections; + +public class ExposedQueueTests +{ + [Fact] + public void ExposedQueue_WhenInitialized_ShouldHaveCorrectMaxCountAndCount() + { + // Arrange + ExposedQueue queue = new(3); + + // Act + int maxCount = queue.MaxCount; + int count = queue.Count; + + // Assert + Assert.Equal(3, maxCount); + Assert.Equal(0, count); + } + + [Fact] + public void ExposedQueue_WhenInitializedWithArray_ShouldHaveCorrectMaxCountAndCount() + { + // Arrange + int[] array = [1, 2, 3]; + const int expectedMaxCount = 3; + const int expectedCount = 3; + ExposedQueue queue = new(array); + + // Act + int maxCount = queue.MaxCount; + int count = queue.Count; + + // Assert + Assert.Equal(expectedMaxCount, maxCount); + Assert.Equal(expectedCount, count); + } + + [Fact] + public void ExposedQueue_WhenInitializedWithArrayAndCount_ShouldHaveCorrectMaxCountAndCount() + { + // Arrange + int[] array = [1, 2, 0, 0]; + const int countParam = 2; + const int expectedMaxCount = 4; + const int expectedCount = 2; + ExposedQueue queue = new(array, countParam); + + // Act + int maxCount = queue.MaxCount; + int count = queue.Count; + + // Assert + Assert.Equal(expectedMaxCount, maxCount); + Assert.Equal(expectedCount, count); + } + + [Fact] + public void ExposedQueue_WhenInitializedWithArrayAndCount_ShouldSetHeadCorrectly() + { + // Arrange + ExposedQueue queue = new([1, 0, 0], 1); + ExposedQueue expected = new([3, 2, 1]); + + // Act + queue.Put(2); + queue.Put(3); + + // Assert + Assert.Equal(expected, queue); + } + + [Fact] + public void ExposedQueue_WhenInitializedWithCountExceedingLength_ShouldThrowException() + { + // Arrange + void CountExceedsLength() => _ = new ExposedQueue([1, 2, 3], 4); + + // Act & Assert + Assert.Throws(CountExceedsLength); + } + + [Fact] + public void ExposedQueue_WhenInitializedWithNegativeCount_ShouldThrowException() + { + // Arrange + void NegativeCount() => _ = new ExposedQueue([1, 2, 3], -1); + + // Act & Assert + Assert.Throws(NegativeCount); + } + + [Fact] + public void AccessIndex_WhenIndexIsNegative_ThrowsException() + { + // Arrange + void AccessNegativeIndex() => _ = new ExposedQueue(3)[-1]; + + // Act & Assert + Assert.Throws(AccessNegativeIndex); + } + + [Fact] + public void AccessIndex_WhenIndexIsGreaterThanCount_ThrowsException() + { + // Arrange + void AccessIndexGreaterThanCount() => _ = new ExposedQueue(3)[4]; + + // Act & Assert + Assert.Throws(AccessIndexGreaterThanCount); + } + + + [Fact] + public void Put_ArrayIsFull_WrapsAround() + { + // Arrange + ExposedQueue queue = new([4, 3, 2, 1]); + ExposedQueue expected = new([5, 4, 3, 2]); + + // Act + queue.Put(5); + + // Assert + Assert.Equal(expected.Count, queue.Count); + Assert.Equal(expected, queue); + } + + [Fact] + public void GetLast_HeadIsUpdated_ReturnsCorrectElement() + { + // Arrange + ExposedQueue queue = new([3, 2, 1]); + + // Act + queue.Put(4); + + // Assert + Assert.Equal(2, queue.GetLast()); + } + + [Fact] + public void GetFirst_HeadIsUpdated_ReturnsFirstElement() + { + // Arrange + ExposedQueue queue = new([3, 2, 1]); + + // Act + queue.Put(4); + + // Assert + Assert.Equal(4, queue.GetFirst()); + } + + [Fact] + public void CopyTo_WhenStartIndexIsNegative_ThrowsException() + { + // Arrange + ExposedQueue queue = new([3, 2, 1]); + + // Assert + Assert.Throws(() => queue.CopyTo(new int[3], -1, 2)); + } + + [Fact] + public void CopyTo_WhenEndIndexIsOutOfBounds_ThrowsException() + { + // Arrange + ExposedQueue queue = new([3, 2, 1]); + + // Assert + Assert.Throws(() => queue.CopyTo(new int[3], 0, 3)); + } + + [Fact] + public void CopyTo_WhenStartIndexIsAfterEndIndex_ThrowsException() + { + // Arrange + ExposedQueue queue = new([3, 2, 1]); + + // Assert + Assert.Throws(() => queue.CopyTo(new int[3], 2, 1)); + } + + [Fact] + public void CopyTo_WhenCalled_CopiesCorrectly() + { + // Arrange + ExposedQueue queue = new([3, 2, 1]); + + // Act + int[] array = new int[3]; + queue.CopyTo(array, 0, 2); + + // Assert + Assert.Equal(array, [3, 2, 1]); + } + + [Fact] + public void CopyTo_DefaultEndIndex_CopiesCorrectly() + { + // Arrange + ExposedQueue queue = new([3, 2, 1]); + + // Act + int[] array = new int[3]; + queue.CopyTo(array, 0); + + // Assert + Assert.Equal(array, [3, 2, 1]); + } + + [Fact] + public void ToArray_WhenCalled_ReturnsCorrectArray() + { + // Arrange + ExposedQueue queue = new([3, 2, 1]); + + // Act + int[] array = queue.ToArray(); + + // Assert + Assert.Equal(array, [3, 2, 1]); + } + + [Fact] + public void Clear_QueueFilled_ClearsQueue() + { + // Arrange + ExposedQueue queue = new([3, 2, 1]); + + // Act + queue.Clear(); + + // Assert + Assert.Empty(queue); + } + + [Theory] + [InlineData(2, true)] + [InlineData(4, false)] + public void Contains_OnElement_ReturnsCorrectAnswer(int element, bool expected) + { + // Arrange + ExposedQueue queue = new([3, 2, 1]); + + // Act + bool contains = queue.Contains(element); + + // Assert + Assert.Equal(expected, contains); + } + + [Fact] + public void Remove_WhenCalledOnMissingElement_ReturnsFalse() + { + // Arrange + ExposedQueue queue = new([3, 2, 1]); + + // Act + bool removed = queue.Remove(4); + + // Assert + Assert.False(removed); + } + + [Fact] + public void Remove_WhenCalledOnExistingElement_RemovesElement() + { + // Arrange + ExposedQueue queue = new([4, 3, 2, 1]); + ExposedQueue expected = new([4, 2, 1]); + + // Act + bool removed = queue.Remove(3); + + // Assert + Assert.True(removed); + Assert.Equal(expected.Count, queue.Count); + Assert.Equal(expected, queue); + } + + [Fact] + public void Put_AfterRemoval_StillHasCorrectHead() + { + // Arrange + ExposedQueue queue = new([4, 3, 1, 2]); + ExposedQueue expected = new([5, 4, 3, 2]); + + // Act + queue.Remove(1); + queue.Put(5); + + // Assert + Assert.Equal(expected.Count, queue.Count); + Assert.Equal(expected, queue); + } + + [Fact] + public void GetEnumerator_ReturnsCorrectEnumerator() + { + // Arrange + ExposedQueue queue = new([3, 2, 1]); + + // Act + List list = new(); + foreach (int item in queue) + list.Add(item); + + // Assert + Assert.Equal(list, [3, 2, 1]); + } +} diff --git a/Aplib.Core.Tests/Collections/OptimizedActivationStackTests.cs b/Aplib.Core.Tests/Collections/OptimizedActivationStackTests.cs new file mode 100644 index 00000000..76f0b144 --- /dev/null +++ b/Aplib.Core.Tests/Collections/OptimizedActivationStackTests.cs @@ -0,0 +1,248 @@ +using Aplib.Core.Collections; +using FluentAssertions; +using System.Collections.Generic; +using System.Linq; + +namespace Aplib.Core.Tests.Collections; + +public class OptimizedActivationStackTests +{ + /// + /// Given an optimized activation stack with activatables, + /// When ActivatableStackItems is enumerated, + /// Then for each activatable there should be an ActivatableStackItem. + /// + [Fact] + public void ActivatableStackItems_WhenActivatablesAreGiven_ShouldContainEncapsulatedActivatables() + { + // Arrange + int[] activatables = [1, 1, 2, 3, 5, 8]; + OptimizedActivationStack activationStack = new(activatables); + + // Act + IEnumerable.StackItem> activatableStackItems + = activationStack.ActivatableStackItems; + + // Assert + IEnumerable items = activatableStackItems.Select(stackItem => stackItem.Data); + items.Should().BeEquivalentTo(activatables); + } + + /// + /// Given an optimized activation stack with items on the stack, + /// When an item is activated that was not activated yet, + /// Then the item should be on top of the stack. + /// + [Fact] + public void Activate_WhenItemIsActivated_ShouldBeOnTop() + { + // Arrange + int[] activatables = [1, 1, 2, 3, 5]; + OptimizedActivationStack activationStack = new(activatables); + + // Activate all stack items. + OptimizedActivationStack.StackItem[] activationStackItems + = activationStack.ActivatableStackItems.ToArray(); + + foreach (OptimizedActivationStack.StackItem stackItem in activationStackItems[0..^1]) + activationStack.Activate(stackItem); + + // Select the first stack item, because it is not on top of the stack. + OptimizedActivationStack.StackItem stackItemToActivate = activationStackItems[^1]; + + // Act + activationStack.Activate(stackItemToActivate); + int topItem = activationStack.Peek(); + + // Assert + topItem.Should().Be(stackItemToActivate.Data); + } + + /// + /// Given an optimized activation stack with an item on the stack, + /// When an item is activated that was already activated (i.e., reactivated), + /// Then the item should be on top of the stack. + /// + [Fact] + public void Activate_WhenItemIsReactivated_ShouldBeOnTop() + { + // Arrange + int[] activatables = [1, 2, 3]; + OptimizedActivationStack activationStack = new(activatables); + + // Activate all stack items. + foreach (OptimizedActivationStack.StackItem stackItem in activationStack.ActivatableStackItems) + activationStack.Activate(stackItem); + + // Select the first stack item, because it is not on top of the stack. + OptimizedActivationStack.StackItem stackItemToActivate + = activationStack.ActivatableStackItems.First(); + + // Act + activationStack.Activate(stackItemToActivate); + int topItem = activationStack.Peek(); + + // Assert + topItem.Should().Be(stackItemToActivate.Data); + } + + /// + /// Given an optimized activation stack, + /// When an item from a different optimized activation stack is activated on this stack, + /// Then an argument exception should be thrown. + /// + [Fact] + public void Activate_WhenActivatedItemIsFromDifferentStack_ShouldThrowArgumentException() + { + // Arrange + int[] activatables = [1, 2, 3]; + OptimizedActivationStack activationStack = new(activatables); + + int[] otherActivatables = [4, 5, 6]; + OptimizedActivationStack otherActivationStack = new(otherActivatables); + OptimizedActivationStack.StackItem stackItemToActivate + = otherActivationStack.ActivatableStackItems.First(); + + // Act + System.Action activateItem = () => activationStack.Activate(stackItemToActivate); + + // Assert + activateItem.Should().Throw(); + } + + /// + /// Given an optimized activation stack with activated items on the stack, + /// When an item is peeked multiple times, + /// Then the peeked item should be the same. + /// + [Fact] + public void Peek_WhenCalledMultipleTimes_ShouldReturnSameItem() + { + // Arrange + int[] activatables = [1, 2, 3]; + OptimizedActivationStack activationStack = new(activatables); + + // Activate all stack items. + foreach (OptimizedActivationStack.StackItem stackItem in activationStack.ActivatableStackItems) + activationStack.Activate(stackItem); + + // Act + int firstPeekedItem = activationStack.Peek(); + int secondPeekedItem = activationStack.Peek(); + + // Assert + firstPeekedItem.Should().Be(secondPeekedItem); + } + + /// + /// Given an optimized activation stack with no activated items on the stack, + /// When an item is peeked, + /// Then an invalid operation exception should be thrown. + /// + [Fact] + public void Peek_WhenStackIsEmpty_ShouldThrowInvalidOperationException() + { + // Arrange + int[] activatables = [1, 2, 3]; + OptimizedActivationStack activationStack = new(activatables); + + // Act + System.Func peekItem = activationStack.Peek; + + // Assert + peekItem.Should().Throw(); + } + + /// + /// Given an optimized activation stack with activated items on the stack, + /// When an item is peeked, + /// Then the count of the optimized activation stack should stay the same. + /// + [Fact] + public void Peek_WhenCalled_CountShouldStayTheSame() + { + // Arrange + int[] activatables = [1, 2, 3]; + OptimizedActivationStack activationStack = new(activatables); + + // Activate all stack items. + foreach (OptimizedActivationStack.StackItem stackItem in activationStack.ActivatableStackItems) + activationStack.Activate(stackItem); + + // Act + int countBeforePeek = activationStack.Count; + _ = activationStack.Peek(); + int countAfterPeek = activationStack.Count; + + // Assert + countBeforePeek.Should().Be(countAfterPeek); + } + + /// + /// Given an optimized activation stack with activated items on the stack, + /// When multiple items are popped in succession, + /// Then the popped items are different. + /// + [Fact] + public void Pop_WhenCalledMultipleTimes_ShouldReturnDifferentItems() + { + // Arrange + int[] activatables = [1, 2, 3]; + OptimizedActivationStack activationStack = new(activatables); + + // Activate all stack items. + foreach (OptimizedActivationStack.StackItem stackItem in activationStack.ActivatableStackItems) + activationStack.Activate(stackItem); + + // Act + int firstPoppedItem = activationStack.Pop(); + int secondPoppedItem = activationStack.Pop(); + + // Assert + firstPoppedItem.Should().NotBe(secondPoppedItem); + } + + /// + /// Given an optimized activation stack with no activated items on the stack, + /// When an item is popped, + /// Then an invalid operation exception should be thrown. + /// + [Fact] + public void Pop_WhenStackIsEmpty_ShouldThrowInvalidOperationException() + { + // Arrange + int[] activatables = [1, 2, 3]; + OptimizedActivationStack activationStack = new(activatables); + + // Act + System.Func popItem = activationStack.Pop; + + // Assert + popItem.Should().Throw(); + } + + /// + /// Given an optimized activation stack with activated items on the stack, + /// When an item is popped, + /// Then the count of the optimized activation stack should be decreased by one. + /// + [Fact] + public void Pop_WhenCalled_CountShouldDecrement() + { + // Arrange + int[] activatables = [1, 2, 3]; + OptimizedActivationStack activationStack = new(activatables); + + // Activate all stack items. + foreach (OptimizedActivationStack.StackItem stackItem in activationStack.ActivatableStackItems) + activationStack.Activate(stackItem); + + // Act + int countBeforePop = activationStack.Count; + _ = activationStack.Pop(); + int countAfterPop = activationStack.Count; + + // Assert + countAfterPop.Should().Be(countBeforePop - 1); + } +} diff --git a/Aplib.Core.Tests/CombinatorTests.cs b/Aplib.Core.Tests/CombinatorTests.cs new file mode 100644 index 00000000..678c1351 --- /dev/null +++ b/Aplib.Core.Tests/CombinatorTests.cs @@ -0,0 +1,553 @@ +using Aplib.Core.Belief.BeliefSets; +using Aplib.Core.Desire.Goals; +using Aplib.Core.Desire.GoalStructures; +using Aplib.Core.Intent.Actions; +using Aplib.Core.Intent.Tactics; +using FluentAssertions; +using Moq; +using static Aplib.Core.Combinators; + +namespace Aplib.Core.Tests; + +public class CombinatorTests +{ + public static readonly object?[][] Metadatas = + [ + [new Metadata(System.Guid.Empty), null, null], + [new Metadata(System.Guid.Empty, "my name"), "my name", null], + [new Metadata(System.Guid.Empty, description: "my description"), null, "my description"], + [new Metadata(System.Guid.Empty, "a name", "a description"), "a name", "a description"] + ]; + + private static void CheckMetadata(string? expectedName, string? expectedDescription, IMetadata actual) + { + actual.Id.Should().BeEmpty(); + actual.Name.Should().Be(expectedName); + actual.Description.Should().Be(expectedDescription); + } + + private static void CheckDefaultMetadata(IMetadata actual) + { + actual.Id.Should().NotBeEmpty(); + actual.Name.Should().BeNull(); + actual.Description.Should().BeNull(); + } + + #region GoalStructure combinator tests + + [Theory] + [MemberData(nameof(Metadatas))] + public void FirstOfGoalStructureCombinator_WhenCalled_GivesExpectedGoalStructure + (Metadata metadata, string? expectedName, string? expectedDescription) + { + // Arrange + Mock> goalStructure1 = new(); + goalStructure1.SetupGet(g => g.Status).Returns(CompletionStatus.Success); + + Mock> goalStructure2 = new(); + goalStructure2.SetupGet(g => g.Status).Returns(CompletionStatus.Success); + + IBeliefSet beliefSet = Mock.Of(); + + // Act + FirstOfGoalStructure firstOfGoalStructure = + FirstOf(metadata, goalStructure1.Object, goalStructure2.Object); + + firstOfGoalStructure.UpdateStatus(beliefSet); + firstOfGoalStructure.UpdateStatus(beliefSet); + + // Assert + CheckMetadata(expectedName, expectedDescription, firstOfGoalStructure.Metadata); + 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 FirstOfGoalStructureCombinator_WithoutMetadata_GivesExpectedGoalStructure() + { + // Arrange + Mock> goalStructure1 = new(); + goalStructure1.SetupGet(g => g.Status).Returns(CompletionStatus.Success); + + Mock> goalStructure2 = new(); + goalStructure2.SetupGet(g => g.Status).Returns(CompletionStatus.Success); + + IBeliefSet beliefSet = Mock.Of(); + + // Act + FirstOfGoalStructure firstOfGoalStructure = + FirstOf(goalStructure1.Object, goalStructure2.Object); + + firstOfGoalStructure.UpdateStatus(beliefSet); + firstOfGoalStructure.UpdateStatus(beliefSet); + + // Assert + CheckDefaultMetadata(firstOfGoalStructure.Metadata); + firstOfGoalStructure.Status.Should().Be(CompletionStatus.Success); + goalStructure1.Verify(x => x.UpdateStatus(It.IsAny()), Times.Once); + goalStructure2.Verify(x => x.UpdateStatus(It.IsAny()), Times.Never); + } + + [Theory] + [MemberData(nameof(Metadatas))] + public void PrimitiveGoalStructureCombinator_WhenCalled_GivesExpectedGoalStructure + (Metadata metadata, string? expectedName, string? expectedDescription) + { + // Arrange + Mock> goal = new(); + goal.Setup(g => g.GetStatus(It.IsAny())).Returns(CompletionStatus.Success); + IBeliefSet beliefSet = Mock.Of(); + + // Act + PrimitiveGoalStructure primitiveGoalStructure = Primitive(metadata, goal.Object); + + primitiveGoalStructure.UpdateStatus(beliefSet); + + // Assert + CheckMetadata(expectedName, expectedDescription, primitiveGoalStructure.Metadata); + primitiveGoalStructure.Status.Should().Be(CompletionStatus.Success); + } + + [Fact] + public void PrimitiveGoalStructureCombinator_WithoutMetadata_GivesExpectedGoalStructure() + { + // Arrange + Mock> goal = new(); + goal.Setup(g => g.GetStatus(It.IsAny())).Returns(CompletionStatus.Success); + IBeliefSet beliefSet = Mock.Of(); + + // Act + PrimitiveGoalStructure primitiveGoalStructure = Primitive(goal.Object); + + primitiveGoalStructure.UpdateStatus(beliefSet); + + // Assert + CheckDefaultMetadata(primitiveGoalStructure.Metadata); + primitiveGoalStructure.Status.Should().Be(CompletionStatus.Success); + } + + [Theory] + [MemberData(nameof(Metadatas))] + public void RepeatGoalStructureCombinator_WhenCalled_GivesExpectedGoalStructure + (Metadata metadata, string? expectedName, string? expectedDescription) + { + // Arrange + Mock> goal = new(); + goal.Setup(g => g.GetStatus(It.IsAny())).Returns(CompletionStatus.Failure); + IBeliefSet beliefSet = Mock.Of(); + + // Act + RepeatGoalStructure repeatGoalStructure = Repeat(metadata, Primitive(goal.Object)); + + repeatGoalStructure.UpdateStatus(beliefSet); + IGoal currentGoal = repeatGoalStructure.GetCurrentGoal(beliefSet); + + // Assert + CheckMetadata(expectedName, expectedDescription, repeatGoalStructure.Metadata); + repeatGoalStructure.Status.Should().Be(CompletionStatus.Unfinished); + currentGoal.Should().Be(goal.Object); + } + + [Fact] + public void RepeatGoalStructureCombinator_WithoutMetadata_GivesExpectedGoalStructure() + { + // Arrange + Mock> goal = new(); + goal.Setup(g => g.GetStatus(It.IsAny())).Returns(CompletionStatus.Failure); + IBeliefSet beliefSet = Mock.Of(); + + // Act + RepeatGoalStructure repeatGoalStructure = Repeat(Primitive(goal.Object)); + + repeatGoalStructure.UpdateStatus(beliefSet); + IGoal currentGoal = repeatGoalStructure.GetCurrentGoal(beliefSet); + + // Assert + CheckDefaultMetadata(repeatGoalStructure.Metadata); + repeatGoalStructure.Status.Should().Be(CompletionStatus.Unfinished); + currentGoal.Should().Be(goal.Object); + } + + [Theory] + [MemberData(nameof(Metadatas))] + public void SequentialGoalStructureCombinator_WhenCalled_GivesExpectedGoalStructure + (Metadata metadata, string? expectedName, string? expectedDescription) + { + Mock> goalStructure1 = new(); + goalStructure1.SetupGet(g => g.Status).Returns(CompletionStatus.Success); + + Mock> goalStructure2 = new(); + goalStructure2.SetupGet(g => g.Status).Returns(CompletionStatus.Success); + + IBeliefSet beliefSet = Mock.Of(); + + // Act + SequentialGoalStructure sequentialGoalStructure = + Seq(metadata, goalStructure1.Object, goalStructure2.Object); + + sequentialGoalStructure.UpdateStatus(beliefSet); + sequentialGoalStructure.UpdateStatus(beliefSet); + + // Assert + CheckMetadata(expectedName, expectedDescription, sequentialGoalStructure.Metadata); + sequentialGoalStructure.Status.Should().Be(CompletionStatus.Success); + goalStructure1.Verify(x => x.UpdateStatus(It.IsAny()), Times.Once); + } + + [Fact] + public void SequentialGoalStructureCombinator_WithoutMetadata_GivesExpectedGoalStructure() + { + Mock> goalStructure1 = new(); + goalStructure1.SetupGet(g => g.Status).Returns(CompletionStatus.Success); + + Mock> goalStructure2 = new(); + goalStructure2.SetupGet(g => g.Status).Returns(CompletionStatus.Success); + + IBeliefSet beliefSet = Mock.Of(); + + // Act + SequentialGoalStructure sequentialGoalStructure = Seq(goalStructure1.Object, goalStructure2.Object); + + sequentialGoalStructure.UpdateStatus(beliefSet); + sequentialGoalStructure.UpdateStatus(beliefSet); + + // Assert + CheckDefaultMetadata(sequentialGoalStructure.Metadata); + sequentialGoalStructure.Status.Should().Be(CompletionStatus.Success); + goalStructure1.Verify(x => x.UpdateStatus(It.IsAny()), Times.Once); + } + + #endregion + + #region Tactic combinator tests + + [Theory] + [MemberData(nameof(Metadatas))] + public void AnyOfTacticCombinator_WhenCalled_GivesExpectedTactic + (Metadata metadata, string? expectedName, string? expectedDescription) + { + // Arrange + Action action1 = new(_ => { }); + Action action2 = new(_ => { }); + // ReSharper disable once ConvertToLocalFunction + System.Func guard = _ => false; + + // Act + AnyOfTactic anyOfTactic = + AnyOf(metadata, guard, Primitive(action1, _ => true), Primitive(action2, _ => false)); + + IAction? selectedAction = anyOfTactic.GetAction(It.IsAny()); + + // Assert + CheckMetadata(expectedName, expectedDescription, anyOfTactic.Metadata); + selectedAction.Should().BeNull(); + } + + [Fact] + public void AnyOfTacticCombinator_WithoutMetadata_GivesExpectedTactic() + { + // Arrange + Action action1 = new(_ => { }); + Action action2 = new(_ => { }); + // ReSharper disable once ConvertToLocalFunction + System.Func guard = _ => false; + + // Act + AnyOfTactic anyOfTactic = + AnyOf(guard, Primitive(action1, _ => true), Primitive(action2, _ => false)); + + IAction? selectedAction = anyOfTactic.GetAction(It.IsAny()); + + // Assert + CheckDefaultMetadata(anyOfTactic.Metadata); + selectedAction.Should().BeNull(); + } + + [Theory] + [MemberData(nameof(Metadatas))] + public void AnyOfTacticCombinator_WithoutGuard_GivesExpectedTactic + (Metadata metadata, string? expectedName, string? expectedDescription) + { + // Arrange + Action action1 = new(_ => { }); + Action action2 = new(_ => { }); + + // Act + AnyOfTactic anyOfTactic = + AnyOf(metadata, Primitive(action1, _ => true), Primitive(action2, _ => false)); + + IAction? selectedAction = anyOfTactic.GetAction(It.IsAny()); + + // Assert + CheckMetadata(expectedName, expectedDescription, anyOfTactic.Metadata); + selectedAction.Should().Be(action1); + } + + [Fact] + public void AnyOfTacticCombinator_WithoutMetadataWithoutGuard_GivesExpectedTactic() + { + // Arrange + Action action1 = new(_ => { }); + Action action2 = new(_ => { }); + + // Act + AnyOfTactic anyOfTactic = AnyOf(Primitive(action1, _ => true), Primitive(action2, _ => false)); + + IAction? selectedAction = anyOfTactic.GetAction(It.IsAny()); + + // Assert + CheckDefaultMetadata(anyOfTactic.Metadata); + selectedAction.Should().Be(action1); + } + + [Theory] + [MemberData(nameof(Metadatas))] + public void FirstOfTacticCombinator_WhenCalled_GivesExpectedTactic + (Metadata metadata, string? expectedName, string? expectedDescription) + { + // Arrange + Action action1 = new(_ => { }); + Action action2 = new(_ => { }); + // ReSharper disable once ConvertToLocalFunction + System.Func guard = _ => false; + + // Act + FirstOfTactic firstOfTactic = + FirstOf(metadata, guard, Primitive(action1, _ => false), Primitive(action2, _ => true)); + + IAction? selectedAction = firstOfTactic.GetAction(It.IsAny()); + + // Assert + CheckMetadata(expectedName, expectedDescription, firstOfTactic.Metadata); + selectedAction.Should().BeNull(); + } + + [Fact] + public void FirstOfTacticCombinator_WithoutMetadata_GivesExpectedTactic() + { + // Arrange + Action action1 = new(_ => { }); + Action action2 = new(_ => { }); + // ReSharper disable once ConvertToLocalFunction + System.Func guard = _ => false; + + // Act + FirstOfTactic firstOfTactic = + FirstOf(guard, Primitive(action1, _ => false), Primitive(action2, _ => true)); + + IAction? selectedAction = firstOfTactic.GetAction(It.IsAny()); + + // Assert + CheckDefaultMetadata(firstOfTactic.Metadata); + selectedAction.Should().BeNull(); + } + + [Theory] + [MemberData(nameof(Metadatas))] + public void FirstOfTacticCombinator_WithoutGuard_GivesExpectedTactic + (Metadata metadata, string? expectedName, string? expectedDescription) + { + // Arrange + Action action1 = new(_ => { }); + Action action2 = new(_ => { }); + + // Act + FirstOfTactic firstOfTactic = + FirstOf(metadata, Primitive(action1, _ => false), Primitive(action2, _ => true)); + + IAction? selectedAction = firstOfTactic.GetAction(It.IsAny()); + + // Assert + CheckMetadata(expectedName, expectedDescription, firstOfTactic.Metadata); + selectedAction.Should().Be(action2); + } + + [Fact] + public void FirstOfTacticCombinator_WithoutMetadataWithoutGuard_GivesExpectedTactic() + { + // Arrange + Action action1 = new(_ => { }); + Action action2 = new(_ => { }); + + // Act + FirstOfTactic firstOfTactic = + FirstOf(Primitive(action1, _ => false), Primitive(action2, _ => true)); + + IAction? selectedAction = firstOfTactic.GetAction(It.IsAny()); + + // Assert + CheckDefaultMetadata(firstOfTactic.Metadata); + selectedAction.Should().Be(action2); + } + + [Theory] + [MemberData(nameof(Metadatas))] + public void PrimitiveTacticCombinator_WhenCalled_GivesExpectedTactic + (Metadata metadata, string? expectedName, string? expectedDescription) + { + // Arrange + Action action = new(_ => { }); + Mock> guard = new(); + guard.SetupSequence(f => f(It.IsAny())).Returns(false).Returns(true); + + // Act + PrimitiveTactic primitiveTactic = Primitive(metadata, action, guard.Object); + + IAction? actionFromFirstCall = primitiveTactic.GetAction(It.IsAny()); + IAction? actionFromSecondCall = primitiveTactic.GetAction(It.IsAny()); + + // Assert + CheckMetadata(expectedName, expectedDescription, primitiveTactic.Metadata); + actionFromFirstCall.Should().BeNull(); + actionFromSecondCall.Should().Be(action); + } + + [Fact] + public void PrimitiveTacticCombinator_WithoutMetadata_GivesExpectedTactic() + { + // Arrange + Action action = new(_ => { }); + Mock> guard = new(); + guard.SetupSequence(f => f(It.IsAny())).Returns(false).Returns(true); + + // Act + PrimitiveTactic primitiveTactic = Primitive(action, guard.Object); + + IAction? actionFromFirstCall = primitiveTactic.GetAction(It.IsAny()); + IAction? actionFromSecondCall = primitiveTactic.GetAction(It.IsAny()); + + // Assert + CheckDefaultMetadata(primitiveTactic.Metadata); + actionFromFirstCall.Should().BeNull(); + actionFromSecondCall.Should().Be(action); + } + + [Theory] + [MemberData(nameof(Metadatas))] + public void PrimitiveTacticCombinator_WithoutGuard_GivesExpectedTactic + (Metadata metadata, string? expectedName, string? expectedDescription) + { + // Arrange + Action action = new(_ => { }); + + // Act + PrimitiveTactic primitiveTactic = Primitive(metadata, action); + + bool isActionable = primitiveTactic.IsActionable(It.IsAny()); + IAction? selectedAction = primitiveTactic.GetAction(It.IsAny()); + + // Assert + CheckMetadata(expectedName, expectedDescription, primitiveTactic.Metadata); + isActionable.Should().BeTrue(); + selectedAction.Should().Be(action); + } + + [Fact] + public void PrimitiveTacticCombinator_WithoutMetadataWithoutGuard_GivesExpectedTactic() + { + // Arrange + Action action = new(_ => { }); + + // Act + PrimitiveTactic primitiveTactic = Primitive(action); + + bool isActionable = primitiveTactic.IsActionable(It.IsAny()); + IAction? selectedAction = primitiveTactic.GetAction(It.IsAny()); + + // Assert + CheckDefaultMetadata(primitiveTactic.Metadata); + isActionable.Should().BeTrue(); + selectedAction.Should().Be(action); + } + + [Theory] + [MemberData(nameof(Metadatas))] + public void PrimitiveTacticCombinator_FromQueryable_GivesExpectedTactic + (Metadata metadata, string? expectedName, string? expectedDescription) + { + // Arrange + Mock> query = new(); + query.SetupSequence(f => f(It.IsAny())).Returns(1).Returns(2); + QueryAction queryAction = new((_, _) => { }, query.Object); + Mock> guard = new(); + guard.SetupSequence(f => f(It.IsAny())).Returns(false).Returns(true); + + // Act + PrimitiveTactic primitiveTactic = Primitive(metadata, queryAction, guard.Object); + + IAction? actionFromFirstCall = primitiveTactic.GetAction(It.IsAny()); + IAction? actionFromSecondCall = primitiveTactic.GetAction(It.IsAny()); + + // Assert + CheckMetadata(expectedName, expectedDescription, primitiveTactic.Metadata); + actionFromFirstCall.Should().BeNull(); + actionFromSecondCall.Should().Be(queryAction); + } + + [Fact] + public void PrimitiveTacticCombinator_FromQueryableWithoutMetadata_GivesExpectedTactic() + { + // Arrange + Mock> query = new(); + query.SetupSequence(f => f(It.IsAny())).Returns(1).Returns(2); + QueryAction queryAction = new((_, _) => { }, query.Object); + Mock> guard = new(); + guard.SetupSequence(f => f(It.IsAny())).Returns(false).Returns(true); + + // Act + PrimitiveTactic primitiveTactic = Primitive(queryAction, guard.Object); + + IAction? actionFromFirstCall = primitiveTactic.GetAction(It.IsAny()); + IAction? actionFromSecondCall = primitiveTactic.GetAction(It.IsAny()); + + // Assert + CheckDefaultMetadata(primitiveTactic.Metadata); + actionFromFirstCall.Should().BeNull(); + actionFromSecondCall.Should().Be(queryAction); + } + + [Theory] + [MemberData(nameof(Metadatas))] + public void PrimitiveTacticCombinator_FromQueryableWithoutGuard_GivesExpectedTactic + (Metadata metadata, string? expectedName, string? expectedDescription) + { + // Arrange + Mock> query = new(); + query.SetupSequence(f => f(It.IsAny())).Returns(1).Returns(2); + QueryAction queryAction = new((_, _) => { }, query.Object); + + // Act + PrimitiveTactic primitiveTactic = Primitive(metadata, queryAction); + + bool isActionable = primitiveTactic.IsActionable(It.IsAny()); + IAction? selectedAction = primitiveTactic.GetAction(It.IsAny()); + + // Assert + CheckMetadata(expectedName, expectedDescription, primitiveTactic.Metadata); + isActionable.Should().BeTrue(); + selectedAction.Should().Be(queryAction); + } + + [Fact] + public void PrimitiveTacticCombinator_FromQueryableWithoutMetadataWithoutGuard_GivesExpectedTactic() + { + // Arrange + Mock> query = new(); + query.SetupSequence(f => f(It.IsAny())).Returns(1).Returns(2); + QueryAction queryAction = new((_, _) => { }, query.Object); + + // Act + PrimitiveTactic primitiveTactic = Primitive(queryAction); + + bool isActionable = primitiveTactic.IsActionable(It.IsAny()); + IAction? selectedAction = primitiveTactic.GetAction(It.IsAny()); + + // Assert + CheckDefaultMetadata(primitiveTactic.Metadata); + isActionable.Should().BeTrue(); + selectedAction.Should().Be(queryAction); + } + + #endregion +} diff --git a/Aplib.Core.Tests/Desire/DesireSetTests.cs b/Aplib.Core.Tests/Desire/DesireSetTests.cs new file mode 100644 index 00000000..80d501ee --- /dev/null +++ b/Aplib.Core.Tests/Desire/DesireSetTests.cs @@ -0,0 +1,188 @@ +using Aplib.Core.Belief.BeliefSets; +using Aplib.Core.Desire.DesireSets; +using Aplib.Core.Desire.Goals; +using Aplib.Core.Desire.GoalStructures; +using FluentAssertions; +using Moq; +using System; + +namespace Aplib.Core.Tests.Desire; + +public class DesireSetTests +{ + /// + /// Given a desire set with finished goals, + /// When the desire set is updated and GetCurrentGoal method is called, + /// Then an invalid operation exception is thrown. + /// + [Theory] + [InlineData(CompletionStatus.Success)] + [InlineData(CompletionStatus.Failure)] + public void GetCurrentGoal_WhenDesireSetIsFinished_ShouldThrowInvalidOperationException( + CompletionStatus finishedMainGoalStatus) + { + // Arrange + Mock> mainGoalStructure = new(); + mainGoalStructure.Setup(g => g.Status).Returns(finishedMainGoalStatus); + + DesireSet desireSet = new(mainGoalStructure.Object); + + // Act + desireSet.Update(It.IsAny()); + Action getCurrentGoal = () => desireSet.GetCurrentGoal(It.IsAny()); + + // Assert + getCurrentGoal.Should().Throw(); + } + + /// + /// Given a desire set with only a main goal structure and no activatables, + /// When the GetCurrentGoal method is called, + /// Then the current goal of the main goal structure should be returned. + /// + [Fact] + public void GetCurrentGoal_WhenOnlyMainGoal_ReturnsMainGoal() + { + // Arrange + IGoal goal = Mock.Of>(); + + Mock> mainGoalStructure = new(); + mainGoalStructure.Setup(g => g.GetCurrentGoal(It.IsAny())).Returns(goal); + + DesireSet desireSet = new(mainGoalStructure.Object); + + // Act + IGoal currentGoal = desireSet.GetCurrentGoal(It.IsAny()); + + // Assert + currentGoal.Should().Be(goal); + } + + /// + /// Given a desire set with some activated side goal structure that is unfinished, + /// When the desire set is updated and GetCurrentGoal method is called, + /// Then the current goal of the side goal structure should be returned. + /// + [Fact] + public void GetCurrentGoal_WhenUnfinishedSideGoalIsActivated_ReturnsSideGoal() + { + // Arrange + IGoal goal = Mock.Of>(); + + Mock> mainGoalStructure = new(); + mainGoalStructure.Setup(g => g.Status).Returns(CompletionStatus.Unfinished); + + Mock> sideGoalStructure = new(); + sideGoalStructure.Setup(g => g.GetCurrentGoal(It.IsAny())).Returns(goal); + sideGoalStructure.Setup(g => g.Status).Returns(CompletionStatus.Unfinished); + + DesireSet desireSet = new(mainGoalStructure.Object, (sideGoalStructure.Object, _ => true)); + + // Act + desireSet.Update(It.IsAny()); + IGoal currentGoal = desireSet.GetCurrentGoal(It.IsAny()); + + // Assert + currentGoal.Should().Be(goal); + } + + /// + /// Given a desire set with an unfinished main goal + /// and some activated side goal structures that are unfinished, + /// When the desire set is updated and GetCurrentGoal method is called, + /// Then the current goal of the main goal structure should be returned. + /// + [Fact] + public void GetCurrentGoal_WhenUnfinishedSideGoalIsNotActivated_ReturnsMainGoal() + { + // Arrange + IGoal goal = Mock.Of>(); + + Mock> mainGoalStructure = new(); + mainGoalStructure.Setup(g => g.GetCurrentGoal(It.IsAny())).Returns(goal); + mainGoalStructure.Setup(g => g.Status).Returns(CompletionStatus.Unfinished); + + Mock> sideGoalStructure = new(); + sideGoalStructure.Setup(g => g.Status).Returns(CompletionStatus.Unfinished); + + DesireSet desireSet = new(mainGoalStructure.Object, (sideGoalStructure.Object, _ => false)); + + // Act + desireSet.Update(It.IsAny()); + IGoal currentGoal = desireSet.GetCurrentGoal(It.IsAny()); + + // Assert + currentGoal.Should().Be(goal); + } + + /// + /// Given a desire set with some activated side goal structures that are unfinished, + /// When the desire set is updated, + /// Then the status should be unfinished. + /// + [Theory] + [InlineData(CompletionStatus.Success)] + [InlineData(CompletionStatus.Failure)] + [InlineData(CompletionStatus.Unfinished)] + public void Update_WhenActivatedSideGoalUnfinished_StatusShouldBeUnfinished(CompletionStatus mainGoalStatus) + { + // Arrange + Mock> mainGoalStructure = new(); + mainGoalStructure.Setup(g => g.Status).Returns(mainGoalStatus); + + Mock> sideGoalStructure = new(); + sideGoalStructure.Setup(g => g.Status).Returns(CompletionStatus.Unfinished); + + DesireSet desireSet = new(mainGoalStructure.Object, (sideGoalStructure.Object, _ => true)); + + // Act + desireSet.Update(It.IsAny()); + CompletionStatus status = desireSet.Status; + + // Assert + status.Should().Be(CompletionStatus.Unfinished); + } + + /// + /// Given a desire set with only a main goal structure and no activatables, + /// When the desire set is updated, + /// Then the status of the main goal structure should be updated. + /// + [Fact] + public void Update_WhenOnlyMainGoal_ShouldUpdateMainGoalStructureStatus() + { + // Arrange + Mock> mainGoalStructure = new(); + DesireSet desireSet = new(mainGoalStructure.Object); + + // Act + desireSet.Update(It.IsAny()); + + // Assert + mainGoalStructure.Verify(g => g.UpdateStatus(It.IsAny()), Times.Once()); + } + + /// + /// Given a desire set with only a main goal, + /// When the desire set is updated, + /// Then the status should be the same as the main goal structure status. + /// + [Theory] + [InlineData(CompletionStatus.Success)] + [InlineData(CompletionStatus.Failure)] + [InlineData(CompletionStatus.Unfinished)] + public void Update_WhenOnlyMainGoal_StatusShouldBeSameAsMainGoal(CompletionStatus mainGoalStatus) + { + // Arrange + Mock> mainGoalStructure = new(); + mainGoalStructure.Setup(g => g.Status).Returns(mainGoalStatus); + DesireSet desireSet = new(mainGoalStructure.Object); + + // Act + desireSet.Update(It.IsAny()); + CompletionStatus status = desireSet.Status; + + // Assert + status.Should().Be(mainGoalStatus); + } +} diff --git a/Aplib.Tests/Desire/GoalStructureTests.cs b/Aplib.Core.Tests/Desire/GoalStructureTests.cs similarity index 89% rename from Aplib.Tests/Desire/GoalStructureTests.cs rename to Aplib.Core.Tests/Desire/GoalStructureTests.cs index a62c4730..b4c2f638 100644 --- a/Aplib.Tests/Desire/GoalStructureTests.cs +++ b/Aplib.Core.Tests/Desire/GoalStructureTests.cs @@ -1,6 +1,6 @@ -using Aplib.Core.Belief; -using Aplib.Core.Desire; +using Aplib.Core.Belief.BeliefSets; using Aplib.Core.Desire.Goals; +using Aplib.Core.Desire.GoalStructures; using FluentAssertions; using Moq; using Moq.Protected; @@ -9,6 +9,21 @@ namespace Aplib.Core.Tests.Desire; public class GoalStructureTests { + [Fact] + public void PrimitiveGoalStructure_ConstructedWithMetadata_HasCorrectMetadata() + { + // Arrange + Metadata metadata = new("My GoalStructure", "The best GoalStructure"); + Mock> goalStructure1 = new(); + + // Act + FirstOfGoalStructure goalStructure = new(metadata, goalStructure1.Object); + + // Assert + goalStructure.Metadata.Name.Should().Be("My GoalStructure"); + goalStructure.Metadata.Description.Should().Be("The best GoalStructure"); + } + [Fact] public void FirstOfGoalStructure_WhenAllGoalsFail_ShouldReturnFailure() { @@ -18,7 +33,7 @@ public void FirstOfGoalStructure_WhenAllGoalsFail_ShouldReturnFailure() goalStructure2.SetupGet(g => g.Status).Returns(CompletionStatus.Failure); IBeliefSet beliefSet = Mock.Of(); - FirstOfGoalStructure firstOfGoalStructure = new([goalStructure1.Object, goalStructure2.Object]); + FirstOfGoalStructure firstOfGoalStructure = new(goalStructure1.Object, goalStructure2.Object); // Act firstOfGoalStructure.UpdateStatus(beliefSet); @@ -35,7 +50,7 @@ public void FirstOfGoalStructure_WhenDisposing_ShouldDisposeChildren() Mock> goalStructure2 = new(); Mock> firstOfGoalStructure - = new([goalStructure1.Object, goalStructure2.Object]) { CallBase = true }; + = new(goalStructure1.Object, goalStructure2.Object) { CallBase = true }; // Act firstOfGoalStructure.Object.Dispose(); @@ -54,7 +69,7 @@ public void FirstOfGoalStructure_WhenFinished_ShouldEarlyExit() goalStructure2.SetupGet(g => g.Status).Returns(CompletionStatus.Success); IBeliefSet beliefSet = Mock.Of(); - FirstOfGoalStructure firstOfGoalStructure = new([goalStructure1.Object, goalStructure2.Object]); + FirstOfGoalStructure firstOfGoalStructure = new(goalStructure1.Object, goalStructure2.Object); // Act firstOfGoalStructure.UpdateStatus(beliefSet); @@ -79,7 +94,7 @@ public void FirstOfGoalStructure_WhenFirstGoalIsFailure_ShouldReturnSecondGoal() .Returns(goal); IBeliefSet beliefSet = Mock.Of(); - FirstOfGoalStructure firstOfGoalStructure = new([goalStructure1.Object, goalStructure2.Object]); + FirstOfGoalStructure firstOfGoalStructure = new(goalStructure1.Object, goalStructure2.Object); // Act firstOfGoalStructure.UpdateStatus(beliefSet); @@ -98,7 +113,7 @@ public void FirstOfGoalStructure_WhenFirstGoalIsFinished_ShouldReturnSuccess() goalStructure1.SetupGet(g => g.Status).Returns(CompletionStatus.Success); Mock> goalStructure2 = new(); IBeliefSet beliefSet = Mock.Of(); - FirstOfGoalStructure firstOfGoalStructure = new([goalStructure1.Object, goalStructure2.Object]); + FirstOfGoalStructure firstOfGoalStructure = new(goalStructure1.Object, goalStructure2.Object); // Act firstOfGoalStructure.UpdateStatus(beliefSet); @@ -123,7 +138,7 @@ public void FirstOfGoalStructure_WhenFirstGoalIsUnfinished_ShouldReturnUnfinishe Mock> goalStructure2 = new(); IBeliefSet beliefSet = Mock.Of(); - FirstOfGoalStructure firstOfGoalStructure = new([goalStructure1.Object, goalStructure2.Object]); + FirstOfGoalStructure firstOfGoalStructure = new(goalStructure1.Object, goalStructure2.Object); // Act firstOfGoalStructure.UpdateStatus(beliefSet); @@ -147,7 +162,7 @@ public void FirstOfGoalStructure_WhenGoalIsUnfinished_ShouldReturnGoal() goalStructure2.SetupGet(g => g.Status).Returns(CompletionStatus.Unfinished); IBeliefSet beliefSet = Mock.Of(); - FirstOfGoalStructure firstOfGoalStructure = new([goalStructure1.Object, goalStructure2.Object]); + FirstOfGoalStructure firstOfGoalStructure = new(goalStructure1.Object, goalStructure2.Object); // Act firstOfGoalStructure.UpdateStatus(beliefSet); @@ -260,7 +275,7 @@ public void SequentialGoalStructure_WhenDisposing_ShouldDisposeChildren() Mock> goalStructure2 = new(); Mock> sequentialGoalStructure - = new([goalStructure1.Object, goalStructure2.Object]) { CallBase = true }; + = new(goalStructure1.Object, goalStructure2.Object) { CallBase = true }; // Act sequentialGoalStructure.Object.Dispose(); @@ -279,7 +294,8 @@ public void SequentialGoalStructure_WhenFirstGoalIsFinished_ShouldNotUseFirstGoa goalStructure2.SetupGet(g => g.Status).Returns(CompletionStatus.Success); IBeliefSet beliefSet = Mock.Of(); - SequentialGoalStructure sequentialGoalStructure = new([goalStructure1.Object, goalStructure2.Object]); + SequentialGoalStructure sequentialGoalStructure = + new(goalStructure1.Object, goalStructure2.Object); // Act sequentialGoalStructure.UpdateStatus(beliefSet); @@ -298,7 +314,8 @@ public void SequentialGoalStructure_WhenFirstGoalIsFinished_ShouldReturnUnfinish goalStructure1.SetupGet(g => g.Status).Returns(CompletionStatus.Success); Mock> goalStructure2 = new(); IBeliefSet beliefSet = Mock.Of(); - SequentialGoalStructure sequentialGoalStructure = new([goalStructure1.Object, goalStructure2.Object]); + SequentialGoalStructure sequentialGoalStructure = + new(goalStructure1.Object, goalStructure2.Object); // Act sequentialGoalStructure.UpdateStatus(beliefSet); @@ -315,7 +332,8 @@ public void SequentialGoalStructure_WhenGoalFails_ShouldReturnFailure() goalStructure1.SetupGet(g => g.Status).Returns(CompletionStatus.Failure); Mock> goalStructure2 = new(); IBeliefSet beliefSet = Mock.Of(); - SequentialGoalStructure sequentialGoalStructure = new([goalStructure1.Object, goalStructure2.Object]); + SequentialGoalStructure sequentialGoalStructure = + new(goalStructure1.Object, goalStructure2.Object); // Act sequentialGoalStructure.UpdateStatus(beliefSet); @@ -327,11 +345,12 @@ public void SequentialGoalStructure_WhenGoalFails_ShouldReturnFailure() [Fact] public void SequentialGoalStructure_WhenProvidingNoGoalStructure_ShouldThrowException() { - // Arrange + // Arrange IGoalStructure[] goalStructures = []; // Act - System.Func> act = () => new SequentialGoalStructure(goalStructures); + System.Func> act = + () => new SequentialGoalStructure(goalStructures); // Assert act.Should().Throw(); diff --git a/Aplib.Tests/Desire/GoalTests.cs b/Aplib.Core.Tests/Desire/GoalTests.cs similarity index 96% rename from Aplib.Tests/Desire/GoalTests.cs rename to Aplib.Core.Tests/Desire/GoalTests.cs index faa3f292..e91b9dad 100644 --- a/Aplib.Tests/Desire/GoalTests.cs +++ b/Aplib.Core.Tests/Desire/GoalTests.cs @@ -1,4 +1,4 @@ -using Aplib.Core.Belief; +using Aplib.Core.Belief.BeliefSets; using Aplib.Core.Desire.Goals; using Aplib.Core.Intent.Actions; using Aplib.Core.Intent.Tactics; @@ -28,7 +28,7 @@ public void Goal_WhenConstructed_ContainsCorrectMetaData() // Act // Does not use helper methods on purpose - Goal goal = new(tactic, heuristicFunction: heuristicFunction, metadata: metadata); + Goal goal = new(metadata, tactic, heuristicFunction: heuristicFunction); // Assert goal.Should().NotBeNull(); @@ -47,6 +47,7 @@ public void Goal_WhenConstructed_DidNotIterateYet() int iterations = 0; Mock> tactic = new(); tactic.Setup(x => x.GetAction(It.IsAny())).Returns(new Action(_ => { iterations++; })); + // Act Goal goal = new TestGoalBuilder().UseTactic(tactic.Object).Build(); @@ -150,7 +151,8 @@ public void GoalConstructor_WhereHeuristicFunctionTypeDiffers_HasEqualBehaviour( ITactic tactic = Mock.Of>(); bool heuristicFunctionBoolean(IBeliefSet _) => goalCompleted; - Goal.HeuristicFunction heuristicFunctionNonBoolean = CommonHeuristicFunctions.Boolean(_ => goalCompleted); + Goal.HeuristicFunction heuristicFunctionNonBoolean = + CommonHeuristicFunctions.Boolean(_ => goalCompleted); Goal goalBoolean = new(tactic, heuristicFunctionBoolean); Goal goalNonBoolean = new(tactic, heuristicFunctionNonBoolean); diff --git a/Aplib.Tests/GlobalUsings.cs b/Aplib.Core.Tests/GlobalUsings.cs similarity index 100% rename from Aplib.Tests/GlobalUsings.cs rename to Aplib.Core.Tests/GlobalUsings.cs diff --git a/Aplib.Core.Tests/Intent/Actions/ActionTests.cs b/Aplib.Core.Tests/Intent/Actions/ActionTests.cs new file mode 100644 index 00000000..3ef8cde7 --- /dev/null +++ b/Aplib.Core.Tests/Intent/Actions/ActionTests.cs @@ -0,0 +1,86 @@ +using Aplib.Core.Belief.BeliefSets; +using Aplib.Core.Intent.Actions; +using FluentAssertions; +using Moq; + +namespace Aplib.Core.Tests.Intent.Actions; + +/// +/// Describes a set of tests for the class. +/// +public class ActionTests +{ + [Fact] + public void Action_WhenConstructed_ContainsCorrectMetaData() + { + // Arrange + const string name = "Action"; + const string description = "A cheap store where I get all my stuff"; + Metadata metadata = new(name, description); + + // Act + Action action = new(metadata, _ => { }); + + // Assert + action.Should().NotBeNull(); + action.Metadata.Name.Should().Be(name); + action.Metadata.Description.Should().Be(description); + } + + [Fact] + public void Action_WithoutDescription_ContainsCorrectMetaData() + { + // Arrange + const string name = "my action"; + Metadata metadata = new(name); + + // Act + Action action = new(metadata, _ => { }); + + // Assert + action.Should().NotBeNull(); + action.Metadata.Name.Should().Be(name); + action.Metadata.Description.Should().BeNull(); + } + + /// + /// Given a side effect action, + /// When the action is executed, + /// Then the result should not be null. + /// + [Fact] + public void Execute_SideEffects_ReturnsCorrectEffect() + { + // Arrange + string? result = null; + IBeliefSet beliefSet = Mock.Of(); + Action action = new(_ => result = "abc"); + + // Act + action.Execute(beliefSet); + + // Assert + result.Should().Be("abc"); + } + + /// + /// 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 Execute_WithGuard_ShouldInvokeQueryAndStoreResult() + { + // Arrange + int result = 0; + IBeliefSet beliefSet = Mock.Of(); + QueryAction action = new(query: _ => 42, effect: (_, query) => result = query); + + // Act + _ = action.Query(beliefSet); + action.Execute(beliefSet); + + // Assert + result.Should().Be(42); + } +} diff --git a/Aplib.Core.Tests/Intent/Tactics/TacticTests.cs b/Aplib.Core.Tests/Intent/Tactics/TacticTests.cs new file mode 100644 index 00000000..773dc475 --- /dev/null +++ b/Aplib.Core.Tests/Intent/Tactics/TacticTests.cs @@ -0,0 +1,429 @@ +using Aplib.Core.Belief.BeliefSets; +using Aplib.Core.Intent.Actions; +using Aplib.Core.Intent.Tactics; +using FluentAssertions; +using Moq; +using System.Collections.Generic; + +namespace Aplib.Core.Tests.Intent.Tactics; + +public class TacticTests +{ + // A subclass of Tactic for testing + public class TestTactic : Tactic + { + public System.Func Guard => _guard; + + public TestTactic(Metadata metadata, System.Func guard) : base(metadata, guard) { } + + public TestTactic(System.Func guard) : base(guard) { } + + public TestTactic(Metadata metadata) : base(metadata) { } + + public TestTactic() { } + + public override IAction GetAction(IBeliefSet beliefSet) + => throw new System.NotImplementedException(); + } + + // A subclass of FirstOfTactic for testing + public class TestFirstOfTactic : FirstOfTactic + { + public System.Func Guard => _guard; + + public LinkedList> SubTactics => _subTactics; + + public TestFirstOfTactic + (Metadata metadata, System.Func guard, params ITactic[] subTactics) + : base(metadata, guard, subTactics) + { + } + + public TestFirstOfTactic(Metadata metadata, params ITactic[] subTactics) + : base(metadata, subTactics) + { + } + + public TestFirstOfTactic(System.Func guard, params ITactic[] subTactics) + : base(guard, subTactics) + { + } + + public TestFirstOfTactic(params ITactic[] subTactics) : base(subTactics) { } + } + + + [Fact] + public void Tactic_WhenConstructed_HasExpectedData() + { + // Arrange + Metadata metadata = It.IsAny(); + System.Func guard = It.IsAny>(); + + // Act + TestTactic tactic = new(metadata, guard); + + // Assert + tactic.Metadata.Should().Be(metadata); + tactic.Guard.Should().Be(guard); + } + + [Fact] + public void Tactic_WithoutMetadata_HasExpectedData() + { + // Arrange + System.Func guard = It.IsAny>(); + + // Act + TestTactic tactic = new(guard); + + // Assert + tactic.Metadata.Id.Should().NotBeEmpty(); + tactic.Metadata.Name.Should().BeNull(); + tactic.Metadata.Description.Should().BeNull(); + tactic.Guard.Should().Be(guard); + } + + [Fact] + public void Tactic_WithoutGuard_HasExpectedData() + { + // Arrange + Metadata metadata = It.IsAny(); + + // Act + TestTactic tactic = new(metadata); + + // Assert + tactic.Metadata.Should().Be(metadata); + tactic.Guard(It.IsAny()).Should().BeTrue(); + } + + [Fact] + public void Tactic_Default_HasExpectedData() + { + // Act + TestTactic tactic = new(); + + // Assert + tactic.Metadata.Id.Should().NotBeEmpty(); + tactic.Metadata.Name.Should().BeNull(); + tactic.Metadata.Description.Should().BeNull(); + tactic.Guard(It.IsAny()).Should().BeTrue(); + } + + /// + /// Given a tactic and an action, + /// When the guard of the tactic returns true, + /// Then an action should be selected. + /// + [Fact] + public void PrimitveTacticExecute_WhenGuardReturnsTrue_ActionIsSelected() + { + // Arrange + Mock> action = new(); + PrimitiveTactic tactic = new(action.Object, guard: _ => true); + + // Act + IAction selectedAction = tactic.GetAction(It.IsAny())!; + + // Assert + selectedAction.Should().NotBeNull(); + } + + /// + /// Given no metadata, + /// When an AnyOfTactic is constructed, + /// Then it has no name, and a random id. + /// + [Fact] + public void AnyOfTactic_WithoutMetadata_ContainsDefaultMetadata() + { + // Act + AnyOfTactic tactic = new(_ => true); + + // Assert + tactic.Metadata.Id.Should().NotBeEmpty(); + tactic.Metadata.Name.Should().BeNull(); + tactic.Metadata.Description.Should().BeNull(); + } + + /// + /// Given a parent of type with two subtactics, + /// When getting the next tactic, + /// Then the result should be the action of an enabled tactic. + /// + [Fact] + public void AnyOfTacticGetAction_WhenASubtacticIsEnabled_ReturnsActionOfEnabledSubtactic() + { + // Arrange + Action action1 = new(_ => { }); + Action action2 = new(_ => { }); + PrimitiveTactic tactic1 = new(action1, _ => true); + PrimitiveTactic tactic2 = new(action2, _ => false); + AnyOfTactic parentTactic = new(tactic1, tactic2); + + // Act + IAction? selectedAction = parentTactic.GetAction(It.IsAny()); + + // Assert + selectedAction.Should().Be(action1); + } + + [Fact] + public void AnyOfTactic_WithFalseGuard_ReturnsNoAction() + { + // Arrange + Action action = new(_ => { }); + PrimitiveTactic tactic = new(action, _ => true); + AnyOfTactic parentTactic = new(_ => false, tactic); + + // Act + IAction? selectedAction = parentTactic.GetAction(It.IsAny()); + + // Assert + selectedAction.Should().BeNull(); + } + + [Fact] + public void FirstOfTacticTactic_WhenConstructed_HasExpectedData() + { + // Arrange + Metadata metadata = It.IsAny(); + System.Func guard = It.IsAny>(); + ITactic[] subTactics = [It.IsAny>()]; + + // Act + TestFirstOfTactic tactic = new(metadata, guard, subTactics); + + // Assert + tactic.Metadata.Should().Be(metadata); + tactic.Guard.Should().Be(guard); + tactic.SubTactics.Should().BeEquivalentTo(subTactics); + } + + [Fact] + public void FirstOfTacticTactic_WithoutGuard_HasExpectedData() + { + // Arrange + Metadata metadata = It.IsAny(); + ITactic[] subTactics = [It.IsAny>()]; + + // Act + TestFirstOfTactic tactic = new(metadata, subTactics); + + // Assert + tactic.Metadata.Should().Be(metadata); + tactic.Guard(It.IsAny()).Should().BeTrue(); + tactic.SubTactics.Should().BeEquivalentTo(subTactics); + } + + [Fact] + public void FirstOfTacticTactic_WithoutMetadata_HasExpectedData() + { + // Arrange + System.Func guard = It.IsAny>(); + ITactic[] subTactics = [It.IsAny>()]; + + // Act + TestFirstOfTactic tactic = new(guard, subTactics); + + // Assert + tactic.Metadata.Id.Should().NotBeEmpty(); + tactic.Metadata.Name.Should().BeNull(); + tactic.Metadata.Description.Should().BeNull(); + tactic.Guard.Should().Be(guard); + tactic.SubTactics.Should().BeEquivalentTo(subTactics); + } + + [Fact] + public void FirstOfTacticTactic_WithoutMetadataWithoutGuard_HasExpectedData() + { + // Arrange + ITactic[] subTactics = [It.IsAny>()]; + + // Act + TestFirstOfTactic tactic = new(subTactics); + + // Assert + tactic.Metadata.Id.Should().NotBeEmpty(); + tactic.Metadata.Name.Should().BeNull(); + tactic.Metadata.Description.Should().BeNull(); + tactic.Guard(It.IsAny()).Should().BeTrue(); + tactic.SubTactics.Should().BeEquivalentTo(subTactics); + } + + /// + /// Given a parent of type with two subtactics, + /// When both subtactic guards are true, + /// Then the result should be the first subtactic. + /// + [Fact] + public void FirstOfTacticGetAction_WhenBothSubtacticsEnabled_ReturnsFirstSubtactic() + { + // Arrange + Action action1 = new(_ => { }); + Action action2 = new(_ => { }); + PrimitiveTactic tactic1 = new(action1, _ => true); + PrimitiveTactic tactic2 = new(action2, _ => true); + FirstOfTactic parentTactic = new(tactic1, tactic2); + + // Act + IAction? selectedAction = parentTactic.GetAction(It.IsAny()); + + // Assert + selectedAction.Should().Be(action1); + } + + /// + /// Given a parent of type with two subtactics, + /// When the first subtactic guard is false and the second is true, + /// Then the result should be the second subtactic. + /// + [Fact] + public void FirstOfTacticGetAction_WhenFirstSubtacticIsDisabled_ReturnsSecondSubtactic() + { + // Arrange + Action action1 = new(_ => { }); + Action action2 = new(_ => { }); + PrimitiveTactic tactic1 = new(action1, _ => false); + PrimitiveTactic tactic2 = new(action2, _ => true); + FirstOfTactic parentTactic = new(tactic1, tactic2); + + // Act + IAction? selectedAction = parentTactic.GetAction(It.IsAny()); + + // Assert + selectedAction.Should().Be(action2); + } + + [Fact] + public void FirstOfTactic_WithFalseGuard_ReturnsNoAction() + { + // Arrange + Action action = new(_ => { }); + PrimitiveTactic tactic = new(action, _ => true); + FirstOfTactic parentTactic = new(_ => false, tactic); + + // Act + IAction? selectedAction = parentTactic.GetAction(It.IsAny()); + + // Assert + selectedAction.Should().BeNull(); + } + + /// + /// Given a primitive tactic with an actionable action, + /// When calling GetAction, + /// Then the result should be the action of the tactic. + /// + [Fact] + public void PrimitiveTacticGetAction_WhenTacticTypeIsPrimitiveAndActionIsActionable_ReturnsAction() + { + // Arrange + Action action = new(_ => { }); + PrimitiveTactic tactic = new(action); + + // Act + IAction? enabledAction = tactic.GetAction(It.IsAny()); + + // Assert + enabledAction.Should().Be(action); + } + + /// + /// Given a primitive tactic with a non-actionable action, + /// When calling GetAction, + /// Then the result should be null. + /// + [Fact] + public void PrimitiveTactic_WhenTacticTypeIsPrimitiveAndActionIsNotActionable_ReturnsNoAction() + { + // Arrange + Action action = new(_ => { }); + PrimitiveTactic tactic = new(action, _ => false); + + // Act + IAction? enabledAction = tactic.GetAction(It.IsAny()); + + // Assert + enabledAction.Should().Be(null); + } + + /// + /// 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 PrimitiveTacticIsActionable_WhenGuardReturnsFalse_ReturnsFalse() + { + // Arrange + PrimitiveTactic tactic = new(It.IsAny>(), _ => false); + + // Act + bool isActionable = tactic.IsActionable(It.IsAny()); + + // Assert + isActionable.Should().BeFalse(); + } + + /// + /// Given a tactic with a guard that returns true and an actionable action, + /// When checking if the tactic is actionable, + /// Then the result should be true. + /// + [Fact] + public void PrimitiveTacticIsActionable_WhenGuardReturnsTrueAndActionIsActionable_ReturnsTrue() + { + // Arrange + Action action = new(_ => { }); + PrimitiveTactic tactic = new(action, _ => true); + + // Act + bool isActionable = tactic.IsActionable(It.IsAny()); + + // Assert + isActionable.Should().BeTrue(); + } + + [Fact] + public void PrimitiveTacticIsActionable_WhenGuardReturnsTrueAndHasQuery_ReturnsCorrectQuery() + { + // Arrange + int result = 0; + QueryAction action = new((_, b) => result = b, _ => 42); + PrimitiveTactic tactic = new(action, _ => true); + IBeliefSet beliefSet = Mock.Of(); + + // Act + bool isActionable = tactic.IsActionable(beliefSet); + tactic.GetAction(beliefSet)!.Execute(beliefSet); + + // Assert + isActionable.Should().BeTrue(); + result.Should().Be(42); + } + + [Fact] + public void PrimitiveTacticIsActionable_WhenGuardReturnsTrueAndHasNullQuery_IsNotActionable() + { + // Arrange + int result = 0; + QueryAction action = new((_, b) => result = b!.Value, + _ => + { + int? x = null; + return x; + } + ); + PrimitiveTactic tactic = new(action, _ => true); + IBeliefSet beliefSet = Mock.Of(); + + // Act + bool isActionable = tactic.IsActionable(beliefSet); + + // Assert + isActionable.Should().BeFalse(); + result.Should().Be(0); + } +} diff --git a/Aplib.Core.Tests/LiftingTests.cs b/Aplib.Core.Tests/LiftingTests.cs new file mode 100644 index 00000000..f3ffa58e --- /dev/null +++ b/Aplib.Core.Tests/LiftingTests.cs @@ -0,0 +1,344 @@ +using Aplib.Core.Belief.BeliefSets; +using Aplib.Core.Desire.DesireSets; +using Aplib.Core.Desire.Goals; +using Aplib.Core.Desire.GoalStructures; +using Aplib.Core.Intent.Actions; +using Aplib.Core.Intent.Tactics; +using FluentAssertions; +using Moq; +using System.Collections.Generic; + +namespace Aplib.Core.Tests; + +/// +/// Contains all the tests related to lifting +/// +public class LiftingTests +{ + public static IEnumerable MetadataMemberData => new object[][] + { + [new Metadata()], + [new Metadata("Kaas", "saaK")], + [new Metadata(name: "Kaas")], + [new Metadata(description: "aap noot mies") ] + }; + + /// + /// Given a query action lifted to a tactic, + /// When compared to the latter, manually created to be trivially correct, + /// Then the two should be equal, but the metadata name should differ. + /// + [Fact] + public void ImplicitLifting_QueryActionLiftedToTactic_DoesNotDifferFromManualTactic() + { + // Arrange + QueryAction action = new((_, _) => { }, _ => 69); + + // Act + Tactic liftedTactic = action; + Tactic manualTactic = new PrimitiveTactic(queryAction: action, _ => true); + + // Assert + liftedTactic.GetAction(It.IsAny()) + .Should().Be(manualTactic.GetAction(It.IsAny())); + // TODO assert guard + } + + /// + /// Given a query action lifted to a tactic, + /// When compared to the latter, manually created to be trivially correct, + /// Then the two should be equal, but the metadata name should differ. + /// + [Theory] + [MemberData(nameof(MetadataMemberData))] + public void ExplicitLifting_QueryActionLiftedToTactic_DoesNotDifferFromManualTactic(Metadata metadata) + { + // Arrange + Action action = It.IsAny>(); + + // Act + Tactic liftedTactic = action.Lift(metadata); + Tactic manualTactic = new PrimitiveTactic(metadata, action, _ => true); + + // Assert + liftedTactic.Should().BeEquivalentTo(manualTactic); + } + + /// + /// Given a query action lifted to a tactic, + /// When compared to the latter, manually created to be trivially correct, + /// Then the two should be equal, but the metadata name should differ. + /// + [Fact] + public void Lifting_IQueryableLiftedToTactic_DoesNotDifferFromManualTactic() + { + // Arrange + Action action = It.IsAny>(); + + // Act + Tactic liftedTactic = action.Lift(); + Tactic manualTactic = new PrimitiveTactic(action, _ => true); + + // Assert + liftedTactic.Should().BeEquivalentTo(manualTactic, config => config.Excluding(o => o.Metadata.Id)); + } + + /// + /// Given an action lifted to a tactic, + /// When compared to the latter, manually created to be trivially correct, + /// Then the two should be equal, but the metadata name should differ. + /// + [Fact] + public void ImplicitLifting_ActionLiftedToTactic_DoesNotDifferFromManualTactic() + { + // Arrange + Action action = It.IsAny>(); + + // Act + Tactic liftedTactic = action; + Tactic manualTactic = new PrimitiveTactic(action, _ => true); + + // Assert + liftedTactic.GetAction(It.IsAny()) + .Should().Be(manualTactic.GetAction(It.IsAny())); + // TODO assert guard + } + + /// + /// Given an action lifted to a tactic, + /// When compared to the latter, manually created to be trivially correct, + /// Then the two should be equal, but the metadata name should differ. + /// + [Theory] + [MemberData(nameof(MetadataMemberData))] + public void ExplicitLifting_ActionLiftedToTactic_DoesNotDifferFromManualTactic(Metadata metadata) + { + // Arrange + Action action = It.IsAny>(); + + // Act + Tactic liftedTactic = action.Lift(metadata); + Tactic manualTactic = new PrimitiveTactic(metadata, action, _ => true); + + // Assert + liftedTactic.Should().BeEquivalentTo(manualTactic); + } + + /// + /// Given a goal lifted to a goal structure, + /// When compared to the latter, manually created to be trivially correct, + /// Then the two should be equal, but the metadata name should differ. + /// + [Fact] + public void ImplicitLifting_GoalLiftedToGoalStructure_DoesNotDifferFromManualTactic() + { + // Arrange + Goal goal = new(It.IsAny>(), _ => true); + + // Act + GoalStructure liftedGoalStructure = goal; + GoalStructure manualGoalStructure = new PrimitiveGoalStructure(goal); + + // Assert + liftedGoalStructure.Should().BeEquivalentTo(manualGoalStructure, config => config + .Excluding(o => o.Metadata.Id)); + } + + /// + /// Given a goal lifted to a goal structure, + /// When compared to the latter, manually created to be trivially correct, + /// Then the two should be equal, but the metadata name should differ. + /// + [Theory] + [MemberData(nameof(MetadataMemberData))] + public void ExplicitLifting_GoalLiftedToGoalStructure_DoesNotDifferFromManualTactic(Metadata metadata) + { + // Arrange + Goal goal = It.IsAny>(); + + // Act + GoalStructure liftedGoalStructure = goal.Lift(metadata); + GoalStructure manualGoalStructure = new PrimitiveGoalStructure(metadata, goal); + + // Assert + liftedGoalStructure.Should().BeEquivalentTo(manualGoalStructure); + } + + /// + /// Given a goal structure lifted to a desire set, + /// When compared to the latter, manually created to be trivially correct, + /// Then the two should be equal, but the metadata name should differ. + /// + [Fact] + public void ImplicitLifting_GoalStructureLiftedToDesireSet_DoesNotDifferFromManualTactic() + { + // Arrange + GoalStructure goalStructure = new PrimitiveGoalStructure(It.IsAny>()); + + // Act + DesireSet liftedDesireSet = goalStructure; + DesireSet manualDesireSet = new(goalStructure.Metadata, goalStructure); + + // Assert + liftedDesireSet.Should().BeEquivalentTo(manualDesireSet, config => config + .Excluding(o => o.Metadata.Id)); + } + + /// + /// Given a goal structure lifted to a desire set, + /// When compared to the latter, manually created to be trivially correct, + /// Then the two should be equal, but the metadata name should differ. + /// + [Theory] + [MemberData(nameof(MetadataMemberData))] + public void ExplicitLifting_GoalStructureLiftedToDesireSet_DoesNotDifferFromManualTactic(Metadata metadata) + { + // Arrange + GoalStructure goalStructure = It.IsAny>(); + + // Act + DesireSet liftedDesireSet = goalStructure.Lift(metadata); + DesireSet manualDesireSet = new(metadata, goalStructure); + + // Assert + liftedDesireSet.Should().BeEquivalentTo(manualDesireSet); + } + + /// + /// Given a goal lifted to a desire set, + /// When compared to the latter, manually created to be trivially correct, + /// Then the two should be equal, but the metadata name should differ. + /// + [Fact] + public void ImplicitLifting_GoalLiftedToDesireSet_DoesNotDifferFromManualTactic() + { + // Arrange + Goal goal = new(It.IsAny>(), _ => true); + + // Act + DesireSet liftedDesireSet = goal; + DesireSet manualDesireSet = new(goal.Metadata, new PrimitiveGoalStructure(goal)); + + // Assert + liftedDesireSet.Should().BeEquivalentTo(manualDesireSet, config => config + .Excluding(o => o.Metadata.Id)); + } + + /// + /// Given a goal lifted to a desire set, + /// When compared to the latter, manually created to be trivially correct, + /// Then the two should be equal, but the metadata name should differ. + /// + [Theory] + [MemberData(nameof(MetadataMemberData))] + public void ExplicitLifting_GoalLiftedToDesireSet_DoesNotDifferFromManualTactic(Metadata metadata) + { + // Arrange + Goal goal = It.IsAny>(); + + // Act + DesireSet liftedDesireSet = goal.Lift().Lift(metadata); + DesireSet manualDesireSet = new(metadata, new PrimitiveGoalStructure(goal)); + + // Assert + liftedDesireSet.Should().BeEquivalentTo(manualDesireSet, config => config); + } + + /// + /// Given an undocumented action which is lifted to a tactic, + /// When compared to the latter, manually created to be trivially correct, + /// Then the two should be equal, but the metadata name should differ. + /// + [Fact] + public void Lifting_UndocumentedActionToTactic_DoesNotDifferFromManualTactic() + { + Mock> undocumentedAction = new(); + + // Act + Tactic liftedTactic = undocumentedAction.Object.Lift(); + Tactic manualTactic = new PrimitiveTactic(undocumentedAction.Object, _ => true); + + // Assert + liftedTactic.Should().BeEquivalentTo(manualTactic, config => config + .Excluding(o => o.Metadata.Id)); + } + + /// + /// Given an undocumented action which is lifted to a tactic, + /// When compared to the latter, manually created to be trivially correct, + /// Then the two should be equal, but the metadata name should differ. + /// + [Fact] + public void Lifting_UndocumentedQueryActionToTactic_DoesNotDifferFromManualTactic() + { + Mock> undocumentedQueryAction = new(); + + // Act + Tactic liftedTactic = undocumentedQueryAction.Object.Lift(); + Tactic manualTactic = new PrimitiveTactic(undocumentedQueryAction.Object, _ => true); + + // Assert + liftedTactic.Should().BeEquivalentTo(manualTactic, config => config + .Excluding(o => o.Metadata.Id)); + } + + /// + /// Given an undocumented goal which is lifted to a goal structure, + /// When compared to the latter, manually created to be trivially correct, + /// Then the two should be equal, but the metadata name should differ. + /// + [Fact] + public void Lifting_UndocumentedGoalToGoalStructure_DoesNotDifferFromManualTactic() + { + // Arrange + Mock> undocumentedGoal = new(); + + // Act + GoalStructure liftedGoalStructure = undocumentedGoal.Object.Lift(); + GoalStructure manualGoalStructure = new PrimitiveGoalStructure(undocumentedGoal.Object); + + // Assert + liftedGoalStructure.Should().BeEquivalentTo(manualGoalStructure, config => config + .Excluding(o => o.Metadata.Id)); + } + + /// + /// Given an undocumented goal lifted to a desire set, + /// When compared to the latter, manually created to be trivially correct, + /// Then the two should be equal, but the metadata name should differ. + /// + [Fact] + public void Lifting_UndocumentedGoalLiftedToDesireSet_DoesNotDifferFromManualTactic() + { + // Arrange + Mock> undocumentedGoal = new(); + + // Act + DesireSet liftedDesireSet = undocumentedGoal.Object.Lift(); + DesireSet manualDesireSet = new(new PrimitiveGoalStructure(undocumentedGoal.Object)); + + // Assert + liftedDesireSet.Should().BeEquivalentTo(manualDesireSet, config => config + .Excluding(o => o.Metadata.Id)); + } + + /// + /// Given an undocumented goal structure which is lifted to a desire set, + /// When compared to the latter, manually created to be trivially correct, + /// Then the two should be equal, but the metadata name should differ. + /// + [Fact] + public void Lifting_UndocumentedGoalStructureToDesireSet_DoesNotDifferFromManualTactic() + { + // Arrange + Mock> undocumentedGoalStructure = new(); + + // Act + DesireSet liftedDesireSet = undocumentedGoalStructure.Object.Lift(); + DesireSet manualDesireSet = new(undocumentedGoalStructure.Object); + + // Assert + liftedDesireSet.Should().BeEquivalentTo(manualDesireSet, config => config + .Excluding(o => o.Metadata.Id)); + } +} diff --git a/Aplib.Tests/MetadataTests.cs b/Aplib.Core.Tests/MetadataTests.cs similarity index 100% rename from Aplib.Tests/MetadataTests.cs rename to Aplib.Core.Tests/MetadataTests.cs diff --git a/Aplib.Tests/Tools/TestGoalBuilder.cs b/Aplib.Core.Tests/Tools/TestGoalBuilder.cs similarity index 89% rename from Aplib.Tests/Tools/TestGoalBuilder.cs rename to Aplib.Core.Tests/Tools/TestGoalBuilder.cs index c125b691..1b088d54 100644 --- a/Aplib.Tests/Tools/TestGoalBuilder.cs +++ b/Aplib.Core.Tests/Tools/TestGoalBuilder.cs @@ -1,4 +1,4 @@ -using Aplib.Core.Belief; +using Aplib.Core.Belief.BeliefSets; using Aplib.Core.Desire.Goals; using Aplib.Core.Intent.Tactics; using Moq; @@ -35,5 +35,5 @@ public TestGoalBuilder WithMetaData(string name, string description) } - public Goal Build() => new(_tactic, _heuristicFunction, metadata: new Metadata(_name, _description)); + public Goal Build() => new(new Metadata(_name, _description), _tactic, _heuristicFunction); } diff --git a/Aplib.Core/BdiAgent.cs b/Aplib.Core/Agents/BdiAgent.cs similarity index 88% rename from Aplib.Core/BdiAgent.cs rename to Aplib.Core/Agents/BdiAgent.cs index 063f3757..aebcc348 100644 --- a/Aplib.Core/BdiAgent.cs +++ b/Aplib.Core/Agents/BdiAgent.cs @@ -1,10 +1,10 @@ -using Aplib.Core.Belief; -using Aplib.Core.Desire; +using Aplib.Core.Belief.BeliefSets; +using Aplib.Core.Desire.DesireSets; using Aplib.Core.Desire.Goals; using Aplib.Core.Intent.Actions; using Aplib.Core.Intent.Tactics; -namespace Aplib.Core +namespace Aplib.Core.Agents { /// /// Represents an agent that performs actions based on goals and beliefs. @@ -12,13 +12,10 @@ namespace Aplib.Core public class BdiAgent : IAgent where TBeliefSet : IBeliefSet { - /// - public CompletionStatus Status => _desireSet.Status; - /// /// Gets the beliefset of the agent. /// - private TBeliefSet _beliefSet { get; } + private readonly TBeliefSet _beliefSet; /// /// Gets the desire of the agent. @@ -26,7 +23,10 @@ public class BdiAgent : IAgent /// /// The desire contains all goal structures and the current goal. /// - private IDesireSet _desireSet { get; } + private readonly IDesireSet _desireSet; + + /// + public CompletionStatus Status => _desireSet.Status; /// /// Initializes a new instance of the class. @@ -50,7 +50,7 @@ public void Update() _beliefSet.UpdateBeliefs(); // Desire - _desireSet.UpdateStatus(_beliefSet); + _desireSet.Update(_beliefSet); if (Status != CompletionStatus.Unfinished) return; IGoal goal = _desireSet.GetCurrentGoal(_beliefSet); diff --git a/Aplib.Core/IAgent.cs b/Aplib.Core/Agents/IAgent.cs similarity index 50% rename from Aplib.Core/IAgent.cs rename to Aplib.Core/Agents/IAgent.cs index c8eeebfc..5eccc3ee 100644 --- a/Aplib.Core/IAgent.cs +++ b/Aplib.Core/Agents/IAgent.cs @@ -1,18 +1,10 @@ -namespace Aplib.Core +namespace Aplib.Core.Agents { /// /// Defines an agent that can play a game. /// - public interface IAgent + public interface IAgent : ICompletable { - /// - /// Gets the status of the agent. - /// - /// - /// This reflects whether the agent has achieved or failed its goals. - /// - public CompletionStatus Status { get; } - /// /// Updates the agent's state and goals. /// diff --git a/Aplib.Core/Belief/Belief.cs b/Aplib.Core/Belief/Belief.cs deleted file mode 100644 index b2539236..00000000 --- a/Aplib.Core/Belief/Belief.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; - -namespace Aplib.Core.Belief -{ - /// - /// The class represents the agent's belief of a single object. - /// Some object reference is used to generate/update an observation - /// (i.e., some piece of information of the game state as perceived by an agent). - /// - /// - /// It implements the interface. - /// It supports implicit conversion to . - /// - /// The type of the object reference used to generate/update the observation. - /// The type of the observation that the belief represents. - public class Belief : IBelief - { - /// - /// The object reference used to generate/update the observation. - /// - private readonly TReference _reference; - - /// - /// A function that takes an object reference and generates/updates an observation. - /// - private readonly Func _getObservationFromReference; - - /// - /// A condition on when the observation should be updated. - /// - private readonly Func _shouldUpdate = () => true; - - /// - /// The observation represented by the belief (i.e., some piece of information of the game state as perceived by an agent). - /// - private TObservation _observation; - - /// - /// Initializes a new instance of the class with an object reference, - /// and a function to generate/update the observation using the object reference. - /// - /// A function that takes an object reference and generates/updates an observation. - /// A function that takes an object reference and generates/updates an observation. - public Belief(TReference reference, Func getObservationFromReference) - { - _reference = reference; - _getObservationFromReference = getObservationFromReference; - _observation = _getObservationFromReference(_reference); - } - - /// - /// Initializes a new instance of the class with an object reference, - /// a function to generate/update the observation using the object reference, - /// and a condition on when the observation should be updated. - /// - /// The object reference used to generate/update the observation. - /// A function that takes an object reference and generates/updates an observation. - /// A condition on when the observation should be updated. - public Belief(TReference reference, Func getObservationFromReference, Func shouldUpdate) - : this(reference, getObservationFromReference) - { - _shouldUpdate = shouldUpdate; - } - - /// - /// Implicit conversion operator to allow a object - /// to be used where a is expected. - /// - /// The object to convert. - public static implicit operator TObservation(Belief belief) => belief._observation; - - /// - /// Generates/updates the observation if the shouldUpdate condition is satisfied. - /// The observation is then updated by calling the getObservationFromReference function. - /// - public virtual void UpdateBelief() - { - if (_shouldUpdate()) _observation = _getObservationFromReference(_reference); - } - } -} diff --git a/Aplib.Core/Belief/BeliefSet.cs b/Aplib.Core/Belief/BeliefSets/BeliefSet.cs similarity index 93% rename from Aplib.Core/Belief/BeliefSet.cs rename to Aplib.Core/Belief/BeliefSets/BeliefSet.cs index d3f40863..96ef5a26 100644 --- a/Aplib.Core/Belief/BeliefSet.cs +++ b/Aplib.Core/Belief/BeliefSets/BeliefSet.cs @@ -1,6 +1,7 @@ -using System.Linq; +using Aplib.Core.Belief.Beliefs; +using System.Linq; -namespace Aplib.Core.Belief +namespace Aplib.Core.Belief.BeliefSets { /// /// The class can be inherited to define a set of beliefs for an agent. @@ -15,7 +16,7 @@ public abstract class BeliefSet : IBeliefSet private readonly IBelief[] _beliefs; /// - /// Initializes a new instance of the class, + /// Initializes a new instance of the class, /// and stores all public fields of type (that have been defined in the inheriting class) in an array. /// All public fields are then automatically updated when calling . /// diff --git a/Aplib.Core/Belief/IBeliefSet.cs b/Aplib.Core/Belief/BeliefSets/IBeliefSet.cs similarity index 86% rename from Aplib.Core/Belief/IBeliefSet.cs rename to Aplib.Core/Belief/BeliefSets/IBeliefSet.cs index 00286185..18419342 100644 --- a/Aplib.Core/Belief/IBeliefSet.cs +++ b/Aplib.Core/Belief/BeliefSets/IBeliefSet.cs @@ -1,4 +1,4 @@ -namespace Aplib.Core.Belief +namespace Aplib.Core.Belief.BeliefSets { /// /// A belief set defines beliefs for an agent. diff --git a/Aplib.Core/Belief/Beliefs/Belief.cs b/Aplib.Core/Belief/Beliefs/Belief.cs new file mode 100644 index 00000000..2e3230be --- /dev/null +++ b/Aplib.Core/Belief/Beliefs/Belief.cs @@ -0,0 +1,139 @@ +using System; + +namespace Aplib.Core.Belief.Beliefs +{ + /// + /// The class represents the agent's belief of a single object. + /// Some object reference is used to generate/update an observation + /// (i.e., some piece of information of the game state as perceived by an agent). + /// + /// + /// It supports implicit conversion to . + /// + /// + /// The type of the object reference used to generate/update the observation. This must be a reference type, + /// be aware that this is not enforced by C# if is an interface. + /// + /// The type of the observation that the belief represents. + public class Belief : IBelief where TReference : class + { + /// + /// The object reference used to generate/update the observation. + /// + protected readonly TReference _reference; + + /// + /// A function that takes an object reference and generates/updates an observation. + /// + protected readonly Func _getObservationFromReference; + + /// + /// A condition on when the observation should be updated. + /// + protected readonly Func _shouldUpdate; + + /// + /// Gets the metadata of the Belief. + /// + /// + /// This metadata may be useful for debugging or logging. + /// + public Metadata Metadata { get; } + + /// + /// The observation represented by the belief (i.e., some piece of information of the game state as perceived by an agent). + /// + public TObservation Observation { get; protected set; } + + /// + /// Initializes a new instance of the class with an object + /// reference, a function to generate/update the observation using the object reference, + /// and a condition on when the observation should be updated. + /// + /// + /// Metadata about this Belief, used to quickly display the goal in several contexts. + /// + /// + /// The object reference used to generate/update the observation. This must be a reference type, be aware + /// that this is not enforced by C# if is an interface. + /// + /// + /// A function that takes an object reference and generates/updates an observation. + /// + /// A condition on when the observation should be updated. + /// + /// Thrown when is not a reference type. + /// + public Belief + ( + Metadata metadata, + TReference reference, + Func getObservationFromReference, + Func shouldUpdate + ) + { + Type referenceType = reference.GetType(); + if (referenceType.IsValueType) + throw new ArgumentException($"{referenceType.FullName} is not a reference type.", nameof(reference)); + + Metadata = metadata; + _reference = reference; + _getObservationFromReference = getObservationFromReference; + Observation = _getObservationFromReference(_reference); + _shouldUpdate = shouldUpdate; + } + + /// + public Belief + ( + TReference reference, + Func getObservationFromReference, + Func shouldUpdate + ) + : this(new Metadata(), reference, getObservationFromReference, shouldUpdate) + { + } + + /// + public Belief + ( + Metadata metadata, + TReference reference, + Func getObservationFromReference + ) + : this(metadata, reference, getObservationFromReference, () => true) + { + } + + /// + public Belief(TReference reference, Func getObservationFromReference) + : this(new Metadata(), reference, getObservationFromReference, () => true) + { + } + + /// + /// Implicit conversion operator to allow a object + /// to be used where a is expected. + /// + /// The object to convert. + public static implicit operator TObservation(Belief belief) => belief.Observation; + + /// + /// Generates/updates the observation if the shouldUpdate condition is satisfied. + /// The observation is then updated by calling the getObservationFromReference function. + /// + public virtual void UpdateBelief() + { + if (_shouldUpdate()) UpdateObservation(); + } + + /// + /// Generates/updates the observation. + /// + protected void UpdateObservation() + => Observation = _getObservationFromReference(_reference); + } +} diff --git a/Aplib.Core/Belief/IBelief.cs b/Aplib.Core/Belief/Beliefs/IBelief.cs similarity index 90% rename from Aplib.Core/Belief/IBelief.cs rename to Aplib.Core/Belief/Beliefs/IBelief.cs index c361f9f0..970e3aa6 100644 --- a/Aplib.Core/Belief/IBelief.cs +++ b/Aplib.Core/Belief/Beliefs/IBelief.cs @@ -1,4 +1,4 @@ -namespace Aplib.Core.Belief +namespace Aplib.Core.Belief.Beliefs { /// /// A belief represents/encapsulates an observation (i.e., piece of information of the game state as perceived by an agent). diff --git a/Aplib.Core/Belief/Beliefs/ListBelief.cs b/Aplib.Core/Belief/Beliefs/ListBelief.cs new file mode 100644 index 00000000..e2dee6eb --- /dev/null +++ b/Aplib.Core/Belief/Beliefs/ListBelief.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Aplib.Core.Belief.Beliefs +{ + /// + /// A convenience variant of to track multiple references in one + /// belief. Both the collection storing the references and the references themselves can be changed after the + /// has been created. + /// + /// + /// A can be implicitly converted to a + /// , which will have the same size as the reference collection the last time that + /// was called, and contain the observation results for + /// each element in the collection. + /// + /// + /// The type of the object references used to generate/update the observation. + /// + /// The type of the observations that the belief represents. + public class ListBelief : Belief, List> + { + /// + /// Initializes a new instance of the class from an object + /// reference collection, a function to generate an observation from an object reference, and optionally an + /// update guard. + /// + /// + /// Metadata about this Belief, used to quickly display the goal in several contexts. + /// + /// + /// The collection of reference objects. The underlying type implementing + /// must be a reference type, note that this is not enforced by C#. + /// + /// + /// A function that takes an object reference and generates an observation. + /// + /// A condition on when the observation should be updated. + /// + /// Thrown when is not a reference type. + /// + public ListBelief + ( + Metadata metadata, + IEnumerable references, + Func getObservationFromReference, + Func shouldUpdate + ) + : base(metadata, references, refer => refer.Select(getObservationFromReference).ToList(), shouldUpdate) + { + } + + /// + public ListBelief + ( + IEnumerable references, + Func getObservationFromReference, + Func shouldUpdate + ) + : this(new Metadata(), references, getObservationFromReference, shouldUpdate) + { + } + + /// + public ListBelief + ( + Metadata metadata, + IEnumerable references, + Func getObservationFromReference + ) + : this(metadata, references, getObservationFromReference, () => true) + { + } + + /// + public ListBelief + (IEnumerable references, Func getObservationFromReference) + : this(new Metadata(), references, getObservationFromReference, () => true) + { + } + } +} diff --git a/Aplib.Core/Belief/Beliefs/MemoryBelief.cs b/Aplib.Core/Belief/Beliefs/MemoryBelief.cs new file mode 100644 index 00000000..fa998226 --- /dev/null +++ b/Aplib.Core/Belief/Beliefs/MemoryBelief.cs @@ -0,0 +1,134 @@ +using Aplib.Core.Collections; +using System; + +namespace Aplib.Core.Belief.Beliefs +{ + /// + /// The class represents the agent's belief of a single object, + /// but with additional "memory" of previous observations. + /// Some object reference is used to generate/update an observation + /// (i.e., some piece of information on the game state as perceived by an agent). + /// This belief also stores a limited amount of previous observations in memory. + /// + /// + /// It supports implicit conversion to . + /// + /// + /// The type of the reference used to generate/update the observation. This must be a reference type, be aware that + /// this is not enforced by C# if is an interface. + /// + /// The type of the observation the belief represents. + public class MemoryBelief : Belief where TReference : class + { + /// + /// A "memorized" resource, from the last time the belief was updated. + /// + protected readonly ExposedQueue _memorizedObservations; + + /// + /// Initializes a new instance of the class with an object + /// reference, a function to generate/update the observation using the object reference, and a condition on when + /// the observation should be updated. Also initializes the memory array with a specified number of slots. + /// + /// + /// Metadata about this Belief, used to quickly display the goal in several contexts. + /// + /// + /// The reference used to generate/update the observation. This must be a reference type, be aware that + /// this is not enforced by C# if is an interface. + /// + /// + /// A function that takes a reference and generates/updates a observation. + /// + /// The number of frames to remember back. + /// + /// A function that sets a condition on when the observation should be updated. + /// + /// + /// Thrown when is not a reference type. + /// + public MemoryBelief( + Metadata metadata, + TReference reference, + Func getObservationFromReference, + int framesToRemember, + Func shouldUpdate + ) + : base(metadata, reference, getObservationFromReference, shouldUpdate) + => _memorizedObservations = new ExposedQueue(framesToRemember); + + /// + public MemoryBelief( + TReference reference, + Func getObservationFromReference, + int framesToRemember, + Func shouldUpdate + ) + : this(new Metadata(), reference, getObservationFromReference, framesToRemember, shouldUpdate) + { + } + + /// + public MemoryBelief( + Metadata metadata, + TReference reference, + Func getObservationFromReference, + int framesToRemember + ) + : this(metadata, reference, getObservationFromReference, framesToRemember, () => true) + { + } + + /// + public MemoryBelief(TReference reference, + Func getObservationFromReference, + int framesToRemember) + : this(new Metadata(), reference, getObservationFromReference, framesToRemember, () => true) + { + } + + /// + /// Generates/updates the observation. + /// Also stores the previous observation in memory. + /// + public override void UpdateBelief() + { + // We use the implicit conversion to TObservation to store the observation. + _memorizedObservations.Put(this); + + if (_shouldUpdate()) UpdateObservation(); + } + + /// + /// Gets the most recently memorized observation. + /// + /// The most recent memory of the observation. + public TObservation GetMostRecentMemory() => _memorizedObservations.GetFirst(); + + /// + /// Gets the memorized observation at a specific index. + /// A higher index means a memory further back in time. + /// + /// The memory of the observation at the specified index. + /// The index of the memory to get. + /// If true, the index will be clamped between 0 and the last memory index. + public TObservation GetMemoryAt(int index, bool clamp = false) + { + int lastMemoryIndex = _memorizedObservations.Count - 1; + if (clamp) + index = Math.Clamp(index, 0, lastMemoryIndex); + else if (index < 0 || index > lastMemoryIndex) + throw new ArgumentOutOfRangeException(nameof(index), $"Index must be between 0 and {lastMemoryIndex}."); + return _memorizedObservations[index]; + } + + /// + /// Gets all the memorized observations. + /// The first element is the newest memory. + /// + /// An array of all the memorized observations. + public TObservation[] GetAllMemories() => _memorizedObservations.ToArray(); + } +} diff --git a/Aplib.Core/Belief/Beliefs/SampledMemoryBelief.cs b/Aplib.Core/Belief/Beliefs/SampledMemoryBelief.cs new file mode 100644 index 00000000..b32640d9 --- /dev/null +++ b/Aplib.Core/Belief/Beliefs/SampledMemoryBelief.cs @@ -0,0 +1,181 @@ +using System; +using static Aplib.Core.Belief.Beliefs.UpdateMode; + +namespace Aplib.Core.Belief.Beliefs +{ + /// + /// The class represents the agent's belief of a single object, + /// but with additional "memory" of previous observations. + /// These observations are sampled at a fixed rate. + /// Some object reference is used to generate/update an observation + /// (i.e., some piece of information on the game state as perceived by an agent). + /// This belief also stores a limited amount of previous observation samples in memory. + /// Optionally, the belief can always store the most recent observation, regardless of the sample rate. + /// + /// + /// It supports implicit conversion to . + /// + /// + /// The type of the reference used to generate/update the observation. This must be a reference type, be + /// aware that this is not enforced by C# if is an interface. + /// + /// The type of the observation the belief represents. + public class SampledMemoryBelief : MemoryBelief + where TReference : class + { + /// + /// The sample interval of the memory (inverse of the sample rate). + /// One observation memory (i.e., snapshot) is stored every -th cycle. + /// + private readonly int _sampleInterval; + + /// + /// Specifies how this sampled memory belief should be updated. + /// + private readonly UpdateMode _updateMode; + + private int _moduloCounter = 0; + + /// + /// The number of cycles that have passed since the last memory sample was stored. + /// + private int ModuloCounter + { + get => _moduloCounter; + set => _moduloCounter = value % _sampleInterval; + } + + /// + /// Initializes a new instance of the class with an + /// object reference, a function to generate/update the observation using the object reference, and a condition + /// on when the observation should be updated. This belief also stores a limited amount of previous observation + /// samples in memory. + /// Optionally, the belief can always store the most recent observation, regardless of the sample rate. + /// + /// + /// Metadata about this goal, used to quickly display the goal in several contexts. + /// + /// + /// The reference used to generate/update the observation. This must be a reference type. + /// + /// + /// A function that takes a reference and generates/updates an observation. + /// + /// + /// The sample interval of the memory. + /// One observation memory (i.e., snapshot) is stored every sampleInterval-th cycle. + /// + /// Specifies how this sampled memory belief should be updated. + /// The number of frames to remember back. + /// + /// A function that sets a condition on when the observation should be updated. + /// + /// + /// Thrown when is not a reference type. + /// + public SampledMemoryBelief + ( + Metadata metadata, + TReference reference, + Func getObservationFromReference, + int sampleInterval, + UpdateMode updateMode, + int framesToRemember, + Func shouldUpdate + ) + : base(metadata, reference, getObservationFromReference, framesToRemember, shouldUpdate) + { + _sampleInterval = sampleInterval; + _updateMode = updateMode; + } + + /// + public SampledMemoryBelief + ( + TReference reference, + Func getObservationFromReference, + int sampleInterval, + UpdateMode updateMode, + int framesToRemember, + Func shouldUpdate + ) + : this + ( + new Metadata(), + reference, + getObservationFromReference, + sampleInterval, + updateMode, + framesToRemember, + shouldUpdate + ) + { + } + + /// + public SampledMemoryBelief + ( + Metadata metadata, + TReference reference, + Func getObservationFromReference, + int sampleInterval, + UpdateMode updateMode, + int framesToRemember + ) + : this + ( + metadata, + reference, + getObservationFromReference, + sampleInterval, + updateMode, + framesToRemember, + () => true + ) + { + } + + /// + public SampledMemoryBelief + ( + TReference reference, + Func getObservationFromReference, + int sampleInterval, + UpdateMode updateMode, + int framesToRemember + ) + : this + ( + new Metadata(), + reference, + getObservationFromReference, + sampleInterval, + updateMode, + framesToRemember, + () => true + ) + { + } + + /// + /// Determines whether the memory should be sampled. + /// One observation memory (i.e., snapshot) is stored every sampleInterval-th cycle. + /// + /// Whether a memory sample should be stored in the current cycle. + private bool ShouldSampleMemory() => ModuloCounter++ == 0; + + /// + /// Generates/updates the observation if applicable. + /// Also stores the previous observation in memory every sampleInterval-th cycle. + /// + public override void UpdateBelief() + { + if (ShouldSampleMemory()) + base.UpdateBelief(); + else if (_updateMode is AlwaysUpdate && _shouldUpdate()) UpdateObservation(); + } + } +} diff --git a/Aplib.Core/Belief/Beliefs/UpdateMode.cs b/Aplib.Core/Belief/Beliefs/UpdateMode.cs new file mode 100644 index 00000000..27d921f6 --- /dev/null +++ b/Aplib.Core/Belief/Beliefs/UpdateMode.cs @@ -0,0 +1,18 @@ +namespace Aplib.Core.Belief.Beliefs +{ + /// + /// Specifies the update mode of a sampled memory belief. + /// + public enum UpdateMode + { + /// + /// Update the observation every cycle. + /// + AlwaysUpdate, + + /// + /// Update the observation whenever a memory sample is stored. + /// + UpdateWhenSampled + } +} diff --git a/Aplib.Core/Belief/MemoryBelief.cs b/Aplib.Core/Belief/MemoryBelief.cs deleted file mode 100644 index df9021b1..00000000 --- a/Aplib.Core/Belief/MemoryBelief.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; - -namespace Aplib.Core.Belief -{ - /// - /// The class represents the agent's belief of a single object, - /// but with additional "memory" of previous observations. - /// Some object reference is used to generate/update an observation - /// (i.e., some piece of information on the game state as perceived by an agent). - /// This belief also stores a limited amount of previous observations in memory. - /// - /// - /// It implements the interface. - /// It supports implicit conversion to . - /// - /// The type of the reference used to generate/update the observation. - /// The type of the observation the belief represents. - public class MemoryBelief : Belief - { - /// - /// A "memorized" resouce, from the last time the belief was updated. - /// - private readonly CircularArray _memorizedObservations; - - /// - /// Initializes a new instance of the class with an object reference, - /// and a function to generate/update the observation using the object reference. - /// Also initializes the memory array with a specified number of slots. - /// - /// The reference used to generate/update the observation. - /// A function that takes a reference and generates/updates a observation. - /// The number of frames to remember back. - public MemoryBelief(TReference reference, Func getObservationFromReference, int framesToRemember) - : base(reference, getObservationFromReference) - { - _memorizedObservations = new(framesToRemember); - } - - /// - /// Initializes a new instance of the class with an object reference, - /// a function to generate/update the observation using the object reference, - /// and a condition on when the observation should be updated. - /// Also initializes the memory array with a specified number of slots. - /// - /// The reference used to generate/update the observation. - /// A function that takes a reference and generates/updates a observation. - /// The number of frames to remember back. - /// A function that sets a condition on when the observation should be updated. - public MemoryBelief(TReference reference, Func getObservationFromReference, int framesToRemember, - Func shouldUpdate) - : base(reference, getObservationFromReference, shouldUpdate) - { - _memorizedObservations = new(framesToRemember); - } - - /// - /// Generates/updates the observation. - /// Also stores the previous observation in memory. - /// - public override void UpdateBelief() - { - // We use the implicit conversion to TObservation to store the observation - _memorizedObservations.Put(this); - base.UpdateBelief(); - } - - /// - /// Gets the most recently memorized observation. - /// - /// The most recent memory of the observation. - public TObservation GetMostRecentMemory() => _memorizedObservations.GetFirst(); - - /// - /// Gets the memorized observation at a specific index. - /// A higher index means a memory further back in time. - /// If the index is out of bounds, returns the element closest to the index that is in bounds. - /// - /// The memory of the observation at the specified index. - public TObservation GetMemoryAt(int index, bool clamp = false) - { - int lastMemoryIndex = _memorizedObservations.Length - 1; - if (clamp) - index = Math.Clamp(index, 0, lastMemoryIndex); - else if (index < 0 || index > lastMemoryIndex) - throw new ArgumentOutOfRangeException(nameof(index), $"Index must be between 0 and {lastMemoryIndex}."); - return _memorizedObservations[index]; - } - - /// - /// Gets all the memorized observations. - /// The first element is the newest memory. - /// - /// An array of all the memorized observations. - public TObservation[] GetAllMemories() - { - // For now, we return the entire array, but with empty elements for the unused slots - // TODO: make it return only the used slots - return _memorizedObservations.ToArray(); - } - } -} - diff --git a/Aplib.Core/CircularArray.cs b/Aplib.Core/Collections/CircularArray.cs similarity index 96% rename from Aplib.Core/CircularArray.cs rename to Aplib.Core/Collections/CircularArray.cs index c1eb4124..91c31e72 100644 --- a/Aplib.Core/CircularArray.cs +++ b/Aplib.Core/Collections/CircularArray.cs @@ -1,4 +1,4 @@ -namespace Aplib.Core +namespace Aplib.Core.Collections { /// /// An array that wraps around when it reaches its end. @@ -6,12 +6,13 @@ namespace Aplib.Core /// public class CircularArray { + private readonly T[] _array; + private int _head; + /// /// The length of the array. /// public int Length { get; private set; } - private readonly T[] _array; - private int _head; /// /// Initializes a new instance of the class. @@ -94,8 +95,7 @@ public T[] ToArray(int start = 0, int end = -1) { end = end == -1 ? Length - 1 : end; T[] result = new T[end - start + 1]; - for (int i = 0; i < result.Length; i++) - result[i] = this[start + i]; + for (int i = 0; i < result.Length; i++) result[i] = this[start + i]; return result; } diff --git a/Aplib.Core/Collections/ExposedQueue.cs b/Aplib.Core/Collections/ExposedQueue.cs new file mode 100644 index 00000000..47b9399f --- /dev/null +++ b/Aplib.Core/Collections/ExposedQueue.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Aplib.Core.Collections +{ + /// + /// A queue with all elements exposed. + /// Functionally works like a queue with indexing. + /// It has a MaxCount and Count. MaxCount being the maximal length of the queue, + /// and Count being the actual number of elements in the queue. + /// + /// + /// When adding an element to a full queue, all other elements are shifted one place like so: + /// [4, 3, 2, 1], Put(5) => [5, 4, 3, 2] + /// + public class ExposedQueue : ICollection + { + /// + /// The length of the array. + /// + public int MaxCount { get; private set; } + + /// + /// Actual number of elements in the array. + /// + public int Count { get; private set; } + + /// + public bool IsReadOnly => false; + + private readonly T[] _array; + private int _head; + + /// + /// Initializes a new empty instance of the class. + /// + /// The maximum size of the queue. + public ExposedQueue(int size) + { + MaxCount = size; + Count = 0; + _array = new T[MaxCount]; + _head = MaxCount - 1; + } + + /// + /// Initializes a new instance of the class + /// with an array to use as basis for the queue. + /// By default, assumes the array is filled. + /// + /// An array to use as the circular array. + /// The number of actual elements in the array. + /// + /// The MaxCount of the queue will be set to the length of the array. + /// If the array is not fully filled, the Count should be specified. + /// + public ExposedQueue(T[] array, int count) + { + if (count > array.Length) + throw new ArgumentOutOfRangeException(nameof(count), "Count cannot exceed the length of the array."); + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count), "Count cannot be negative."); + + MaxCount = array.Length; + _array = array; + _head = MaxCount - 1; + Count = count; + } + + /// + public ExposedQueue(T[] array) + : this(array, array.Length) + { + } + + /// + /// Gets the element at the specified index. Throws an exception if the index is out of bounds. + /// + /// The index of the element to get. + /// The element at the specified index. + /// + /// Thrown when the index is out of range. + /// + public T this[int index] + { + get + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException(nameof(index)); + return _array[(index + _head + 1) % MaxCount]; + } + private set + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException(nameof(index)); + _array[(index + _head + 1) % MaxCount] = value; + } + } + + /// + /// Puts an element at the start of the queue. + /// + /// The element to add to the queue. + public void Put(T value) + { + _array[_head] = value; + DecrementHead(); + if (Count < MaxCount) + Count++; + } + + /// + public void Add(T item) => Put(item); + + /// + /// Gets the element at the end of the queue. + /// + /// The element at the end of the queue. + public T GetLast() => _array[_head]; + + /// + /// Gets the first element of the queue. + /// + /// The first element of the queue. + public T GetFirst() => this[0]; + + /// + /// Copies the ExposedQueue to an array. + /// The head should be the last element of the array. + /// Copies from start to end inclusive. + /// + /// The array to copy to." + /// The start index of the range to copy. + /// The end index of the range to copy. + /// The ExposedQueue as a regular array. + public void CopyTo(T[] array, int arrayIndex, int endIndex) + { + if (arrayIndex < 0 || arrayIndex >= Count) + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + if (endIndex < 0 || endIndex >= Count) + throw new ArgumentOutOfRangeException(nameof(endIndex)); + if (arrayIndex > endIndex) + throw new ArgumentException("Start index must be less than or equal to end index."); + + for (int i = 0; i < endIndex - arrayIndex + 1; i++) + array[i] = this[arrayIndex + i]; + } + + /// + public void CopyTo(T[] array, int arrayIndex) => CopyTo(array, arrayIndex, arrayIndex + Count - 1); + + /// + /// Converts the ExposedQueue to an array. + /// + /// The start index of the range to convert. + /// The end index of the range to convert. + /// An array containing the elements within the specified range. + public T[] ToArray(int start, int end) + { + if (start < 0 || start >= Count) + throw new ArgumentOutOfRangeException(nameof(start), "Start index must be within the bounds of the array."); + if (end < 0 || end >= Count) + throw new ArgumentOutOfRangeException(nameof(end), "End index must be within the bounds of the array."); + T[] result = new T[end - start + 1]; + CopyTo(result, start, end); + return result; + } + + /// + /// Converts the ExposedQueue to an array. Only returns the used slots. + /// + /// An array containing the elements within the specified range. + public T[] ToArray() => ToArray(0, Count - 1); + + /// + public void Clear() + { + for (int i = 0; i < MaxCount; i++) + _array[i] = default!; + _head = MaxCount - 1; + Count = 0; + } + + /// + public bool Contains(T item) + { + for (int i = 0; i < Count; i++) + if (this[i]!.Equals(item)) + return true; + return false; + } + + /// + /// Removes the specified item from the queue and shifts remaining elements to the left. + /// For example, given the queue [4, 3, 2, 1], if you call Remove(3), the resulting queue will be [4, 2, 1]. + /// + /// The item to remove. + /// True if the item was successfully removed; otherwise, false. + /// + /// The MaxCount will not change, but the Count will decrease by one. + /// + public bool Remove(T item) + { + for (int i = 0; i < Count; i++) + { + if (this[i]!.Equals(item)) + { + RemoveAt(i); + return true; + } + } + + return false; + } + + /// + public IEnumerator GetEnumerator() + { + for (int i = 0; i < Count; i++) + yield return this[i]; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Decrements the head of the array. + /// + private void DecrementHead() => _head = (_head - 1 + MaxCount) % MaxCount; + + /// + /// Removes the element at the specified index. + /// Shifts all other elements to the left. + /// + /// The index of the element to remove. + private void RemoveAt(int index) + { + for (int i = index; i < Count - 1; i++) + this[i] = this[i + 1]; + Count--; + } + } +} diff --git a/Aplib.Core/Collections/OptimizedActivationStack.cs b/Aplib.Core/Collections/OptimizedActivationStack.cs new file mode 100644 index 00000000..f2e6072c --- /dev/null +++ b/Aplib.Core/Collections/OptimizedActivationStack.cs @@ -0,0 +1,239 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Aplib.Core.Collections +{ + /// + /// A stack that has a predefined set of items that can be activated + /// (i.e., pushed on top of the stack). + /// When an item that is already on the stack is activated, it is reactivated + /// (i.e., moved to the top of the stack). + /// + /// + /// The allows for O(1) activation and + /// reactivation of an arbitrary stack item. + /// + /// The type of the items that are put on the stack. + public class OptimizedActivationStack + { + /// + /// The top item on the stack. + /// + private StackItem? _top; + + /// + /// Gets the activatable stack items. + /// + /// + /// The stack items are exposed, since they should be accessible from the outside to + /// provide O(1) activation of a stack item with . + /// + public IEnumerable ActivatableStackItems { get; } + + private int _count = 0; + + /// + /// Gets the number of items that are currently activated (i.e., on the stack). + /// + /// + /// Thrown when the stack count is negative. + /// " + public int Count + { + get => _count; + private set + { + if (value < 0) throw new System.InvalidOperationException("The stack count cannot be negative."); + + _count = value; + } + } + + /// + /// Initializes an optimized activation stack with a set of activatable data. + /// + /// A set of activatable items that could be pushed on the stack. + public OptimizedActivationStack(T[] activatables) + { + // Setup the activatable stack items. + ActivatableStackItems = activatables.Select(activatable => new StackItem(activatable, this)); + } + + /// + /// Activates an item (i.e., pushes an item on top of the stack). + /// If the pushed item is already on the stack, + /// it is extracted from the stack before it is put on top again. + /// + /// + /// The stack item that is pushed on top of the stack (i.e., it is activated). + /// + /// + /// Thrown when an item is pushed that belongs to a different stack. + /// + public void Activate(StackItem item) + { + if (item.ActivationStack != this) + throw new System.ArgumentException( + "Cannot push an item that is not an activatable of this activation stack." + ); + + // Handle the case when the stack is empty. + if (Count == 0) + { + _top = item; + item.IsActive = true; + Count++; + return; + } + + // If the item is already on the stack, remove it before adding it on top again. + // We also don't increment the count in this case, as no new item is added to the stack. + if (item.IsActive) + item.RemoveFromStack(); + else + Count++; + + // Push the new item on top of the stack. + item.PushOnStackAfter(_top!); + _top = item; + } + + /// + /// Peeks the top item from the stack. + /// + /// The top item. + /// Thrown when the stack is empty. + public T Peek() + { + if (_top is null) throw new System.InvalidOperationException("The stack is empty."); + + return _top.Data; + } + + /// + /// Pops the top item from the stack. + /// + /// The popped item. + /// Thrown when the stack is empty. + public T Pop() + { + if (_top is null) throw new System.InvalidOperationException("The stack is empty."); + + // Pop the top item from the stack. + StackItem _oldTop = _top; + _top = _top.Previous; + _oldTop.RemoveFromStack(); + + Count--; + + return _oldTop.Data; + } + + /// + /// Represents (i.e., encapsulates) an item on the activation stack. + /// + /// + /// This class is public, because the whole stack item should be accessible from the outside to + /// provide O(1) activation of a stack item with . + /// + public sealed class StackItem + { + /// + /// Gets the data that this stack item represents. + /// + public T Data { get; } + + /// + /// Gets the activation stack instance that this stack item belongs to. + /// + public OptimizedActivationStack ActivationStack { get; } + + /// + /// Gets or sets the previous (below) item on the stack. + /// + public StackItem? Previous { get; set; } + + /// + /// Gets or sets the next (above) item on the stack. + /// + public StackItem? Next { get; set; } + + /// + /// Gets or sets a value indicating whether the item is currently on the stack. + /// + public bool IsActive { get; set; } = false; + + /// + /// Creates a stack item for the class. + /// + /// The data to put on the stack. + /// The activation stack instance that this stack item belongs to. + public StackItem(T data, OptimizedActivationStack activationStack) + { + Data = data; + ActivationStack = activationStack; + } + + /// + /// Links this item before another item. + /// + /// The item that should be on top. + private void SetNext(StackItem? item) + { + Next = item; + + if (item is not null) item.Previous = this; + } + + /// + /// Links this item after another item. + /// + /// The item that should be below. + private void SetPrevious(StackItem? item) + { + Previous = item; + + if (item is not null) item.Next = this; + } + + /// + /// Pushes an item that is not on the stack yet after another item that is already on the stack. + /// + /// An item that is already on the stack. + /// + /// Thrown when an item is pushed after an item that is not on the same stack, + /// when an item is already on the stack, + /// or when an item is pushed after an item that is not on the stack. + /// + public void PushOnStackAfter(StackItem item) + { + if (ActivationStack != item.ActivationStack) + throw new System.ArgumentException( + "Cannot push an item after an item that is not an activatable of the same stack." + ); + if (IsActive) throw new System.ArgumentException("Cannot push an item that is already on the stack."); + if (!item.IsActive) + throw new System.ArgumentException("Cannot push an item after an item that is not on the stack."); + + SetNext(item.Next); + SetPrevious(item); + + IsActive = true; + } + + /// + /// Safely remove the item from the stack. + /// + public void RemoveFromStack() + { + Previous?.SetNext(Next); + Next?.SetPrevious(Previous); + + Previous = null; + Next = null; + + IsActive = false; + } + } + } +} diff --git a/Aplib.Core/Combinators.cs b/Aplib.Core/Combinators.cs new file mode 100644 index 00000000..62034200 --- /dev/null +++ b/Aplib.Core/Combinators.cs @@ -0,0 +1,157 @@ +using Aplib.Core.Belief.BeliefSets; +using Aplib.Core.Desire.Goals; +using Aplib.Core.Desire.GoalStructures; +using Aplib.Core.Intent.Actions; +using Aplib.Core.Intent.Tactics; + +namespace Aplib.Core +{ + /// + /// Convenience class containing static methods for creating goal structures and tactics. + /// + public static class Combinators + { + #region GoalStructure combinators + + /// + public static FirstOfGoalStructure FirstOf + (IMetadata metadata, params IGoalStructure[] children) + where TBeliefSet : IBeliefSet => + new(metadata, children); + + /// + public static FirstOfGoalStructure FirstOf(params IGoalStructure[] children) + where TBeliefSet : IBeliefSet => + new(children); + + /// + public static PrimitiveGoalStructure Primitive + (IMetadata metadata, IGoal goal) + where TBeliefSet : IBeliefSet => + new(metadata, goal); + + /// + public static PrimitiveGoalStructure Primitive(IGoal goal) + where TBeliefSet : IBeliefSet => + new(goal); + + /// + public static RepeatGoalStructure Repeat + (IMetadata metadata, IGoalStructure goalStructure) + where TBeliefSet : IBeliefSet => + new(metadata, goalStructure); + + /// + public static RepeatGoalStructure Repeat(IGoalStructure goalStructure) + where TBeliefSet : IBeliefSet => + new(goalStructure); + + /// + public static SequentialGoalStructure Seq + (IMetadata metadata, params IGoalStructure[] children) + where TBeliefSet : IBeliefSet => + new(metadata, children); + + /// + public static SequentialGoalStructure Seq(params IGoalStructure[] children) + where TBeliefSet : IBeliefSet => + new(children); + + #endregion + + #region Tactic combinators + + /// + public static AnyOfTactic AnyOf + (IMetadata metadata, System.Func guard, params ITactic[] subTactics) + where TBeliefSet : IBeliefSet => + new(metadata, guard, subTactics); + + /// + public static AnyOfTactic AnyOf + (System.Func guard, params ITactic[] subTactics) + where TBeliefSet : IBeliefSet => + new(guard, subTactics); + + /// + public static AnyOfTactic AnyOf + (IMetadata metadata, params ITactic[] subTactics) + where TBeliefSet : IBeliefSet => + new(metadata, subTactics); + + /// + public static AnyOfTactic AnyOf(params ITactic[] subTactics) + where TBeliefSet : IBeliefSet => + new(subTactics); + + /// + public static FirstOfTactic FirstOf + (IMetadata metadata, System.Func guard, params ITactic[] subTactics) + where TBeliefSet : IBeliefSet => + new(metadata, guard, subTactics); + + /// + public static FirstOfTactic FirstOf + (System.Func guard, params ITactic[] subTactics) + where TBeliefSet : IBeliefSet => + new(guard, subTactics); + + /// + public static FirstOfTactic FirstOf + (IMetadata metadata, params ITactic[] subTactics) + where TBeliefSet : IBeliefSet => + new(metadata, subTactics); + + /// + public static FirstOfTactic FirstOf(params ITactic[] subTactics) + where TBeliefSet : IBeliefSet => + new(subTactics); + + /// + public static PrimitiveTactic Primitive + (IMetadata metadata, IAction action, System.Func guard) + where TBeliefSet : IBeliefSet => + new(metadata, action, guard); + + /// + public static PrimitiveTactic Primitive + (IAction action, System.Func guard) + where TBeliefSet : IBeliefSet => + new(action, guard); + + /// + public static PrimitiveTactic Primitive(IMetadata metadata, IAction action) + where TBeliefSet : IBeliefSet => + new(metadata, action); + + /// + public static PrimitiveTactic Primitive(IAction action) + where TBeliefSet : IBeliefSet => + new(action); + + /// + public static PrimitiveTactic Primitive + (IMetadata metadata, IQueryable query, System.Func guard) + where TBeliefSet : IBeliefSet => + new(metadata, query, guard); + + /// + public static PrimitiveTactic Primitive + (IQueryable query, System.Func guard) + where TBeliefSet : IBeliefSet => + new(query, guard); + + /// + public static PrimitiveTactic Primitive(IMetadata metadata, IQueryable query) + where TBeliefSet : IBeliefSet => + new(metadata, query); + + /// + public static PrimitiveTactic Primitive(IQueryable query) + where TBeliefSet : IBeliefSet => + new(query); + + #endregion + } +} diff --git a/Aplib.Core/Desire/DesireSet.cs b/Aplib.Core/Desire/DesireSet.cs deleted file mode 100644 index e79b028f..00000000 --- a/Aplib.Core/Desire/DesireSet.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Aplib.Core.Belief; -using Aplib.Core.Desire.Goals; - -namespace Aplib.Core.Desire -{ - /// - public class DesireSet : IDesireSet - where TBeliefSet : IBeliefSet - { - /// - /// Stores the main goal structure of the agent. - /// - private IGoalStructure _mainGoal { get; } - - /// - public CompletionStatus Status => _mainGoal.Status; - - /// - /// Initializes a new instance of the class. - /// - /// The main goal structure that the agent needs to complete. - public DesireSet(IGoalStructure mainGoal) - => _mainGoal = mainGoal; - - /// - public IGoal GetCurrentGoal(TBeliefSet beliefSet) - => _mainGoal.GetCurrentGoal(beliefSet); - - /// - public void UpdateStatus(TBeliefSet beliefSet) - => _mainGoal.UpdateStatus(beliefSet); - } -} diff --git a/Aplib.Core/Desire/DesireSets/DesireSet.cs b/Aplib.Core/Desire/DesireSets/DesireSet.cs new file mode 100644 index 00000000..068722fe --- /dev/null +++ b/Aplib.Core/Desire/DesireSets/DesireSet.cs @@ -0,0 +1,132 @@ +using Aplib.Core.Belief.BeliefSets; +using Aplib.Core.Collections; +using Aplib.Core.Desire.Goals; +using Aplib.Core.Desire.GoalStructures; +using System.Linq; + +namespace Aplib.Core.Desire.DesireSets +{ + /// + public class DesireSet : IDesireSet, IDocumented + where TBeliefSet : IBeliefSet + { + /// + public IMetadata Metadata { get; } + + /// + /// Stores the main goal structure of the agent. + /// + private readonly IGoalStructure _mainGoal; + + /// + /// Stores the side goal structures of the agent. + /// Each of these goal structures has an accompanying guard that must be fulfilled to + /// activate the goal structure (i.e., push the goal structure on top of the goal structure stack). + /// All active goal structures of the agent that still need to be finished are pushed on the stack. + /// + private readonly OptimizedActivationStack + <(IGoalStructure goalStructure, System.Func guard)> _goalStructureStack; + + /// + /// If there are no goal structures left to be completed, the status of this desire set is set to the main goal status. + /// + public CompletionStatus Status + => _goalStructureStack.Count == 0 ? _mainGoal.Status : CompletionStatus.Unfinished; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Metadata about this GoalStructure, used to quickly display the goal in several contexts. + /// + /// The main goal structure that the agent needs to complete. + /// The side goal structures that could be activated during the agent playthrough. + public DesireSet( + IMetadata metadata, + IGoalStructure mainGoal, + params (IGoalStructure goalStructure, System.Func guard)[] sideGoals + ) + { + Metadata = metadata; + _mainGoal = mainGoal; + _goalStructureStack = new(sideGoals); + + // Push the main goal structure on the stack. + _goalStructureStack.Activate(new((_mainGoal, _ => false), _goalStructureStack)); + } + + /// + /// + /// DesireSet{TBeliefSet}(IMetadata,IGoalStructure{TBeliefSet},(IGoalStructure{TBeliefSet}, + /// System.Func{TBeliefSet,bool})[]) + /// + /// + public DesireSet( + IGoalStructure mainGoal, + params (IGoalStructure goalStructure, System.Func guard)[] sideGoals + ) : this(new Metadata(), mainGoal, sideGoals) + { } + + /// + /// Pushes side goal structures on the stack if their guard is fulfilled. + /// + /// The belief set to check the guards of the goal structures with. + private void ActivateRelevantGoalStructures(TBeliefSet beliefSet) + { + // Filter all the goal structures by their guards. + var itemsToActivate = _goalStructureStack.ActivatableStackItems + .Where(item => item.Data.guard(beliefSet)); + + // (Re)activate the filtered goal structures. + foreach (var item in itemsToActivate) _goalStructureStack.Activate(item); + } + + /// + public IGoal GetCurrentGoal(TBeliefSet beliefSet) + { + IGoalStructure currentGoalStructure = _goalStructureStack.Peek().goalStructure; + + return currentGoalStructure.GetCurrentGoal(beliefSet); + } + + /// + /// Activates side goal structures when their guard is satisfied, and updates the activation stack + /// by popping goal structures from the top of the stack when they are finished. + /// + /// The belief set of the agent. + public void Update(TBeliefSet beliefSet) + { + ActivateRelevantGoalStructures(beliefSet); + + // Each loop, either the size of the stack is decremented or the loop is exited early. + while (_goalStructureStack.Count > 0) + { + IGoalStructure currentGoalStructure = _goalStructureStack.Peek().goalStructure; + + currentGoalStructure.UpdateStatus(beliefSet); + + // Early exit when an unfinished goal structure is found on the stack, + // because we are not interested in updating the status of goal structures that are not relevant right now. + if (currentGoalStructure.Status == CompletionStatus.Unfinished) return; + + // If the current goal structure is finished, pop it from the stack. + _goalStructureStack.Pop(); + } + } + + /// + /// Implicitly lifts a goal into a desire set. + /// + /// + /// The most logically matching desire set, wrapping around . + public static implicit operator DesireSet(Goal goal) => goal.Lift().Lift(); + + /// + /// Implicitly lifts a goal structure a desire set. + /// + /// + /// The most logically matching desire set, wrapping around . + public static implicit operator DesireSet(GoalStructure goalStructure) => + goalStructure.Lift(); + } +} diff --git a/Aplib.Core/Desire/IDesireSet.cs b/Aplib.Core/Desire/DesireSets/IDesireSet.cs similarity index 88% rename from Aplib.Core/Desire/IDesireSet.cs rename to Aplib.Core/Desire/DesireSets/IDesireSet.cs index 88c487de..ef54277e 100644 --- a/Aplib.Core/Desire/IDesireSet.cs +++ b/Aplib.Core/Desire/DesireSets/IDesireSet.cs @@ -1,7 +1,7 @@ -using Aplib.Core.Belief; +using Aplib.Core.Belief.BeliefSets; using Aplib.Core.Desire.Goals; -namespace Aplib.Core.Desire +namespace Aplib.Core.Desire.DesireSets { /// /// Represents a set of goals that the agent has. @@ -22,6 +22,6 @@ public interface IDesireSet : ICompletable /// Updates the status of this . /// /// The belief set of the agent. - void UpdateStatus(TBeliefSet beliefSet); + void Update(TBeliefSet beliefSet); } } diff --git a/Aplib.Core/Desire/FirstOfGoalStructure.cs b/Aplib.Core/Desire/GoalStructures/FirstOfGoalStructure.cs similarity index 79% rename from Aplib.Core/Desire/FirstOfGoalStructure.cs rename to Aplib.Core/Desire/GoalStructures/FirstOfGoalStructure.cs index f697e93b..988faa37 100644 --- a/Aplib.Core/Desire/FirstOfGoalStructure.cs +++ b/Aplib.Core/Desire/GoalStructures/FirstOfGoalStructure.cs @@ -1,9 +1,9 @@ -using Aplib.Core.Belief; +using Aplib.Core.Belief.BeliefSets; using Aplib.Core.Desire.Goals; using System; using System.Collections.Generic; -namespace Aplib.Core.Desire +namespace Aplib.Core.Desire.GoalStructures { /// /// Represents a goal structure that will complete if any of its children complete. @@ -15,21 +15,28 @@ namespace Aplib.Core.Desire public class FirstOfGoalStructure : GoalStructure, IDisposable where TBeliefSet : IBeliefSet { - private IEnumerator> _childrenEnumerator { get; } + private readonly IEnumerator> _childrenEnumerator; /// /// Initializes a new instance of the class. /// + /// + /// Metadata about this GoalStructure, used to quickly display the goal in several contexts. + /// /// The children of the goal structure. - public FirstOfGoalStructure(params IGoalStructure[] children) : base(children) + public FirstOfGoalStructure(IMetadata metadata, params IGoalStructure[] children) : base(metadata, children) { _childrenEnumerator = _children.GetEnumerator(); _childrenEnumerator.MoveNext(); _currentGoalStructure = _childrenEnumerator.Current; } + /// + public FirstOfGoalStructure(params IGoalStructure[] children) : this(new Metadata(), children) { } + /// - public override IGoal GetCurrentGoal(TBeliefSet beliefSet) => _currentGoalStructure!.GetCurrentGoal(beliefSet); + public override IGoal GetCurrentGoal(TBeliefSet beliefSet) + => _currentGoalStructure!.GetCurrentGoal(beliefSet); /// public override void UpdateStatus(TBeliefSet beliefSet) @@ -39,6 +46,7 @@ public override void UpdateStatus(TBeliefSet beliefSet) while (true) { if (Status == CompletionStatus.Success) return; + _currentGoalStructure!.UpdateStatus(beliefSet); switch (_currentGoalStructure.Status) diff --git a/Aplib.Core/Desire/GoalStructure.cs b/Aplib.Core/Desire/GoalStructures/GoalStructure.cs similarity index 57% rename from Aplib.Core/Desire/GoalStructure.cs rename to Aplib.Core/Desire/GoalStructures/GoalStructure.cs index c3a70c3b..2662afd1 100644 --- a/Aplib.Core/Desire/GoalStructure.cs +++ b/Aplib.Core/Desire/GoalStructures/GoalStructure.cs @@ -1,23 +1,26 @@ -using Aplib.Core.Belief; +using Aplib.Core.Belief.BeliefSets; using Aplib.Core.Desire.Goals; using System.Collections.Generic; -namespace Aplib.Core.Desire +namespace Aplib.Core.Desire.GoalStructures { /// /// Describes a structure of goals that need to be fulfilled. /// - public abstract class GoalStructure : IGoalStructure + public abstract class GoalStructure : IGoalStructure, IDocumented where TBeliefSet : IBeliefSet { /// - public CompletionStatus Status { get; protected set; } + public IMetadata Metadata { get; } /// /// The children of the goal structure. /// protected readonly IEnumerable> _children; + /// + public CompletionStatus Status { get; protected set; } + /// /// The goal structure that is currently being fulfilled. /// @@ -26,8 +29,18 @@ public abstract class GoalStructure : IGoalStructure /// /// Initializes a new instance of the class. /// + /// + /// Metadata about this GoalStructure, used to quickly display the goal in several contexts. + /// /// The children of the goal structure. - protected GoalStructure(IEnumerable> children) => _children = children; + protected GoalStructure(IMetadata metadata, IEnumerable> children) + { + _children = children; + Metadata = metadata; + } + + /// + protected GoalStructure(IEnumerable> children) : this(new Metadata(), children) { } /// /// Gets the current goal using the given . @@ -41,5 +54,12 @@ public abstract class GoalStructure : IGoalStructure /// /// The belief set of the agent. public abstract void UpdateStatus(TBeliefSet beliefSet); + + /// + /// Implicitly lifts a goal into a goal structure. + /// + /// + /// The most logically matching goal structure, wrapping around . + public static implicit operator GoalStructure(Goal goal) => goal.Lift(); } } diff --git a/Aplib.Core/Desire/IGoalStructure.cs b/Aplib.Core/Desire/GoalStructures/IGoalStructure.cs similarity index 92% rename from Aplib.Core/Desire/IGoalStructure.cs rename to Aplib.Core/Desire/GoalStructures/IGoalStructure.cs index 00e3913d..4ea30cc0 100644 --- a/Aplib.Core/Desire/IGoalStructure.cs +++ b/Aplib.Core/Desire/GoalStructures/IGoalStructure.cs @@ -1,7 +1,7 @@ -using Aplib.Core.Belief; +using Aplib.Core.Belief.BeliefSets; using Aplib.Core.Desire.Goals; -namespace Aplib.Core.Desire +namespace Aplib.Core.Desire.GoalStructures { /// /// Represents a goal structure. diff --git a/Aplib.Core/Desire/PrimitiveGoalStructure.cs b/Aplib.Core/Desire/GoalStructures/PrimitiveGoalStructure.cs similarity index 69% rename from Aplib.Core/Desire/PrimitiveGoalStructure.cs rename to Aplib.Core/Desire/GoalStructures/PrimitiveGoalStructure.cs index a9289ca3..839dd6c9 100644 --- a/Aplib.Core/Desire/PrimitiveGoalStructure.cs +++ b/Aplib.Core/Desire/GoalStructures/PrimitiveGoalStructure.cs @@ -1,8 +1,8 @@ -using Aplib.Core.Belief; +using Aplib.Core.Belief.BeliefSets; using Aplib.Core.Desire.Goals; using System; -namespace Aplib.Core.Desire +namespace Aplib.Core.Desire.GoalStructures { /// /// Represents a goal structure that will complete if any of its children complete. @@ -21,8 +21,15 @@ public class PrimitiveGoalStructure : GoalStructure /// /// Initializes a new instance of the class. /// + /// + /// Metadata about this GoalStructure, used to quickly display the goal in several contexts. + /// /// The goal to fulfill. - public PrimitiveGoalStructure(IGoal goal) : base(Array.Empty>()) => _goal = goal; + public PrimitiveGoalStructure(IMetadata metadata, IGoal goal) + : base(metadata, Array.Empty>()) => _goal = goal; + + /// + public PrimitiveGoalStructure(IGoal goal) : this(new Metadata(), goal) { } /// public override IGoal GetCurrentGoal(TBeliefSet beliefSet) => _goal; diff --git a/Aplib.Core/Desire/RepeatGoalStructure.cs b/Aplib.Core/Desire/GoalStructures/RepeatGoalStructure.cs similarity index 69% rename from Aplib.Core/Desire/RepeatGoalStructure.cs rename to Aplib.Core/Desire/GoalStructures/RepeatGoalStructure.cs index 772971e5..bb27e54a 100644 --- a/Aplib.Core/Desire/RepeatGoalStructure.cs +++ b/Aplib.Core/Desire/GoalStructures/RepeatGoalStructure.cs @@ -1,9 +1,9 @@ -using Aplib.Core.Belief; +using Aplib.Core.Belief.BeliefSets; using Aplib.Core.Desire.Goals; using System.Collections.Generic; using static Aplib.Core.CompletionStatus; -namespace Aplib.Core.Desire +namespace Aplib.Core.Desire.GoalStructures { /// /// Represents a goal structure that will complete if any of its children complete. @@ -18,10 +18,16 @@ public class RepeatGoalStructure : GoalStructure /// /// Initializes a new instance of the class. /// - /// The goalstructure to repeat - public RepeatGoalStructure(IGoalStructure goalStructure) : base( - new List> { goalStructure }) => - _currentGoalStructure = goalStructure; + /// + /// Metadata about this goal, used to quickly display the goal in several contexts. + /// + /// The GoalStructure to repeat. + public RepeatGoalStructure(IMetadata metadata, IGoalStructure goalStructure) + : base(metadata, new List> { goalStructure }) + => _currentGoalStructure = goalStructure; + + /// + public RepeatGoalStructure(IGoalStructure goalStructure) : this(new Metadata(), goalStructure) { } /// public override IGoal GetCurrentGoal(TBeliefSet beliefSet) => _currentGoalStructure!.Status switch diff --git a/Aplib.Core/Desire/SequentialGoalStructure.cs b/Aplib.Core/Desire/GoalStructures/SequentialGoalStructure.cs similarity index 81% rename from Aplib.Core/Desire/SequentialGoalStructure.cs rename to Aplib.Core/Desire/GoalStructures/SequentialGoalStructure.cs index 1dc86437..ad93c015 100644 --- a/Aplib.Core/Desire/SequentialGoalStructure.cs +++ b/Aplib.Core/Desire/GoalStructures/SequentialGoalStructure.cs @@ -1,9 +1,9 @@ -using Aplib.Core.Belief; +using Aplib.Core.Belief.BeliefSets; using Aplib.Core.Desire.Goals; using System; using System.Collections.Generic; -namespace Aplib.Core.Desire +namespace Aplib.Core.Desire.GoalStructures { /// /// Represents a sequential goal structure. @@ -19,23 +19,34 @@ public class SequentialGoalStructure : GoalStructure, ID /// /// Gets or sets the enumerator for the children of the goal structure. /// - private IEnumerator> _childrenEnumerator { get; } + private readonly IEnumerator> _childrenEnumerator; /// /// Initializes a new instance of the class. /// + /// + /// Metadata about this GoalStructure, used to quickly display the goal in several contexts. + /// /// The children of the goal structure. - public SequentialGoalStructure(params IGoalStructure[] children) : base(children) + public SequentialGoalStructure(IMetadata metadata, params IGoalStructure[] children) + : base(metadata, children) { if (children.Length <= 0) throw new ArgumentException("Collection of children is empty", nameof(children)); + _childrenEnumerator = _children.GetEnumerator(); _childrenEnumerator.MoveNext(); _currentGoalStructure = _childrenEnumerator.Current; } + /// + public SequentialGoalStructure(params IGoalStructure[] children) : this(new Metadata(), children) + { + } + /// - public override IGoal GetCurrentGoal(TBeliefSet beliefSet) => _currentGoalStructure!.GetCurrentGoal(beliefSet); + public override IGoal GetCurrentGoal(TBeliefSet beliefSet) + => _currentGoalStructure!.GetCurrentGoal(beliefSet); /// public override void UpdateStatus(TBeliefSet beliefSet) @@ -45,6 +56,7 @@ public override void UpdateStatus(TBeliefSet beliefSet) while (true) { if (Status == CompletionStatus.Success) return; + _currentGoalStructure!.UpdateStatus(beliefSet); switch (_currentGoalStructure.Status) diff --git a/Aplib.Core/Desire/Goals/CommonHeuristicFunctions.cs b/Aplib.Core/Desire/Goals/CommonHeuristicFunctions.cs index 0d741fbb..da0a9494 100644 --- a/Aplib.Core/Desire/Goals/CommonHeuristicFunctions.cs +++ b/Aplib.Core/Desire/Goals/CommonHeuristicFunctions.cs @@ -1,4 +1,4 @@ -using Aplib.Core.Belief; +using Aplib.Core.Belief.BeliefSets; using System; namespace Aplib.Core.Desire.Goals @@ -23,7 +23,8 @@ public static Goal.HeuristicFunction Boolean(Func /// 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 }; + 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 diff --git a/Aplib.Core/Desire/Goals/Goal.cs b/Aplib.Core/Desire/Goals/Goal.cs index 7dfc5012..6a2644a7 100644 --- a/Aplib.Core/Desire/Goals/Goal.cs +++ b/Aplib.Core/Desire/Goals/Goal.cs @@ -1,4 +1,5 @@ -using Aplib.Core.Belief; +using Aplib.Core.Belief.BeliefSets; +using Aplib.Core.Desire.GoalStructures; using Aplib.Core.Intent.Tactics; using System; @@ -6,15 +7,33 @@ 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 - /// the testing. + /// applying the tactic. Goals are combined in a , and are used to + /// prepare tests or do the testing. /// /// /// The belief set of the agent. - public class Goal : IGoal + public class Goal : IGoal, IDocumented 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. + /// + protected const double DefaultEpsilon = 0.005d; + + /// + /// The goal is considered to be completed, when the distance of the is below + /// this value. + /// + protected readonly double _epsilon; + + /// + /// 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. @@ -22,16 +41,12 @@ public class Goal : IGoal /// public delegate Heuristics HeuristicFunction(TBeliefSet beliefSet); - /// - /// Gets the metadata of the goal. - /// - /// - /// This metadata may be useful for debugging or logging. - /// - public Metadata Metadata { get; } + /// + public IMetadata Metadata { get; } /// - /// The used to achieve this , which is executed during every + /// The used to achieve this , which is + /// executed during every /// iteration of the BDI cycle. /// public ITactic Tactic { get; } @@ -39,69 +54,76 @@ public class Goal : IGoal /// public CompletionStatus Status { get; protected set; } - /// - /// 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 - /// completed. - /// - /// - protected HeuristicFunction _heuristicFunction; - /// /// 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 goal is considered to be completed, when the distance of the is below - /// this value. - /// /// /// Metadata about this goal, used to quickly display the goal in several contexts. /// + /// The tactic used to approach this goal. + /// The heuristic function which defines whether a goal is reached. + /// + /// The goal is considered to be completed, when the distance of the + /// is below this value. + /// public Goal ( + IMetadata metadata, ITactic tactic, HeuristicFunction heuristicFunction, - double epsilon = 0.005d, - Metadata? metadata = null + double epsilon = DefaultEpsilon ) { + Metadata = metadata; Tactic = tactic; _heuristicFunction = heuristicFunction; _epsilon = epsilon; - Metadata = metadata ?? new Metadata(); + } + + /// + public Goal(ITactic tactic, HeuristicFunction heuristicFunction, double epsilon = DefaultEpsilon) + : this(new Metadata(), tactic, heuristicFunction, epsilon) + { } /// /// 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 goal is considered to be completed, when the distance of the is below - /// this value. - /// /// /// Metadata about this goal, used to quickly display the goal in several contexts. /// - public Goal(ITactic tactic, Func predicate, double epsilon = 0.005d, Metadata? metadata = null) + /// The tactic used to approach this goal. + /// + /// The heuristic function (or specifically predicate) which defines whether a goal is reached. + /// + /// + /// The goal is considered to be completed, when the distance of the + /// is below this value. + /// + public Goal + ( + IMetadata metadata, + ITactic tactic, + Func predicate, + double epsilon = DefaultEpsilon + ) + : this(metadata, tactic, CommonHeuristicFunctions.Boolean(predicate), epsilon) + { + } + + /// + public Goal(ITactic tactic, Func predicate, double epsilon = DefaultEpsilon) + : this(new Metadata(), tactic, predicate, epsilon) { - Tactic = tactic; - _heuristicFunction = CommonHeuristicFunctions.Boolean(predicate); - _epsilon = epsilon; - Metadata = metadata ?? new Metadata(); } /// /// 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); + public virtual Heuristics DetermineCurrentHeuristics(TBeliefSet beliefSet) + => _heuristicFunction.Invoke(beliefSet); /// /// Tests whether the goal has been achieved, bases on the and the diff --git a/Aplib.Core/Desire/Goals/IGoal.cs b/Aplib.Core/Desire/Goals/IGoal.cs index ef7db90b..4969b62e 100644 --- a/Aplib.Core/Desire/Goals/IGoal.cs +++ b/Aplib.Core/Desire/Goals/IGoal.cs @@ -1,4 +1,4 @@ -using Aplib.Core.Belief; +using Aplib.Core.Belief.BeliefSets; using Aplib.Core.Intent.Tactics; namespace Aplib.Core.Desire.Goals @@ -14,7 +14,7 @@ public interface IGoal : ICompletable /// The used to achieve this , which is executed during every /// iteration of the BDI cycle. /// - public ITactic Tactic { get; } + ITactic Tactic { get; } /// /// Gets the of the current state of the game. diff --git a/Aplib.Core/IDocumented.cs b/Aplib.Core/IDocumented.cs new file mode 100644 index 00000000..e9c1f8c7 --- /dev/null +++ b/Aplib.Core/IDocumented.cs @@ -0,0 +1,16 @@ +namespace Aplib.Core +{ + /// + /// Represents an object that contains general information on an instance, such as . + /// + public interface IDocumented + { + /// + /// Gets the metadata of the instance. + /// + /// + /// This metadata may be useful for debugging or logging. + /// + public IMetadata Metadata { get; } + } +} diff --git a/Aplib.Core/IMetadata.cs b/Aplib.Core/IMetadata.cs new file mode 100644 index 00000000..30184434 --- /dev/null +++ b/Aplib.Core/IMetadata.cs @@ -0,0 +1,29 @@ +using System; + +namespace Aplib.Core +{ + /// + /// A collection of generic metadata for unique instances which should help + /// visualise the instance with human-readable information. + /// + /// + /// This metadata may be useful for debugging or logging. + /// + public interface IMetadata + { + /// + /// Gets the unique identifier of the instance. + /// + public Guid Id { get; } + + /// + /// Gets the name used to display the instance. + /// + public string? Name { get; } + + /// + /// Gets the description used to describe the instance. + /// + public string? Description { get; } + } +} diff --git a/Aplib.Core/Intent/Actions/Action.cs b/Aplib.Core/Intent/Actions/Action.cs index df379464..37023b4e 100644 --- a/Aplib.Core/Intent/Actions/Action.cs +++ b/Aplib.Core/Intent/Actions/Action.cs @@ -1,5 +1,4 @@ -using Aplib.Core.Belief; -using System; +using Aplib.Core.Belief.BeliefSets; namespace Aplib.Core.Intent.Actions { @@ -7,69 +6,34 @@ namespace Aplib.Core.Intent.Actions /// Describes an action that can be executed and guarded. /// /// The belief set of the agent. - public class Action : IAction + public class Action : IAction, IDocumented where TBeliefSet : IBeliefSet { - /// - /// Gets the metadata of the action. - /// - /// - /// This metadata may be useful for debugging or logging. - /// - public Metadata Metadata { get; } - /// /// Gets or sets the effect of the action. /// - protected System.Action _effect { get; set; } + protected readonly System.Action _effect; - /// - /// Gets or sets the guard of the action. - /// - protected Func _guard { get; set; } = _ => true; + /// + public IMetadata Metadata { get; } /// /// Initializes a new instance of the class. /// = - /// The effect of the action. /// /// Metadata about this action, used to quickly display the action in several contexts. /// - public Action(System.Action effect, Metadata? metadata = null) + /// The effect of the action. + public Action(IMetadata metadata, System.Action effect) { + Metadata = metadata; _effect = effect; - Metadata = metadata ?? new Metadata(); } - /// - /// Initializes a new instance of the class. - /// - /// The effect of the action. - /// The guard of the action. - /// - /// Metadata about this action, used to quickly display the action in several contexts. - /// - public Action(System.Action effect, Func guard, Metadata? metadata = null) : this(effect, metadata) => _guard = guard; - - /// - /// Initializes a new empty instance of the class. - /// - /// Only meant for internal use - /// - /// Metadata about this action, used to quickly display the action in several contexts. - /// - protected Action(Metadata? metadata) - { - _effect = _ => { }; - _guard = _ => false; - Metadata = metadata ?? new Metadata(); - } + /// + public Action(System.Action effect) : this(new Metadata(), effect) { } /// public virtual void Execute(TBeliefSet beliefSet) => _effect(beliefSet); - - - /// - public virtual bool IsActionable(TBeliefSet beliefSet) => _guard(beliefSet); } } diff --git a/Aplib.Core/Intent/Actions/GuardedAction.cs b/Aplib.Core/Intent/Actions/GuardedAction.cs deleted file mode 100644 index bc194658..00000000 --- a/Aplib.Core/Intent/Actions/GuardedAction.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Aplib.Core.Belief; -using Aplib.Core.Intent.Actions; - -namespace Aplib.Core.Intent.Actions -{ - /// - /// Describes an action that can be executed and guarded with a query that stores the result of the guard. - /// The result can be used in the effect. - /// - /// The belief set of the agent. - /// The type of the query of the action - public class GuardedAction : Action - where TBeliefSet : IBeliefSet - { - /// - /// Gets or sets the result of the guard. - /// - protected TQuery? _storedGuardResult { get; set; } - - /// - /// Gets or sets the effect of the action. - /// - protected new System.Action _effect { get; set; } - - /// - /// Gets or sets the guard of the action. - /// - protected new System.Func _guard { get; set; } - - /// - /// Initializes a new instance of the class. - /// - /// The effect of the action. - /// The guard of the action. - /// - /// Metadata about this action, used to quickly display the action in several contexts. - /// - public GuardedAction(System.Action effect, System.Func guard, Metadata? metadata = null) - : base(metadata) - { - _effect = effect; - _guard = guard; - } - - /// - public override void Execute(TBeliefSet beliefSet) => _effect(beliefSet, _storedGuardResult!); - - /// - public override bool IsActionable(TBeliefSet beliefSet) - { - _storedGuardResult = _guard(beliefSet); - return _storedGuardResult is not null; - } - } -} diff --git a/Aplib.Core/Intent/Actions/IAction.cs b/Aplib.Core/Intent/Actions/IAction.cs index 8192c12f..ee0aa1d0 100644 --- a/Aplib.Core/Intent/Actions/IAction.cs +++ b/Aplib.Core/Intent/Actions/IAction.cs @@ -1,4 +1,4 @@ -using Aplib.Core.Belief; +using Aplib.Core.Belief.BeliefSets; namespace Aplib.Core.Intent.Actions { @@ -14,12 +14,5 @@ public interface IAction /// /// The belief set on which the action is executed. void Execute(TBeliefSet beliefSet); - - /// - /// Guard the action against unwanted execution. The result is stored and can be used in the effect. - /// - /// The belief set on which the action is executed. - /// True if the action is actionable, false otherwise. - bool IsActionable(TBeliefSet beliefSet); } } diff --git a/Aplib.Core/Intent/Actions/IQueryable.cs b/Aplib.Core/Intent/Actions/IQueryable.cs new file mode 100644 index 00000000..775f4df9 --- /dev/null +++ b/Aplib.Core/Intent/Actions/IQueryable.cs @@ -0,0 +1,19 @@ +using Aplib.Core.Belief.BeliefSets; + +namespace Aplib.Core.Intent.Actions +{ + /// + /// Represents an interface for executing queries on a belief set. + /// + /// The type of the query object. + public interface IQueryable : IAction + where TBeliefSet : IBeliefSet + { + /// + /// Executes a query on the specified belief set. + /// + /// The belief set to query. + /// A boolean value indicating whether the query executed successfully or not. + public bool Query(TBeliefSet beliefSet); + } +} diff --git a/Aplib.Core/Intent/Actions/QueryAction.cs b/Aplib.Core/Intent/Actions/QueryAction.cs new file mode 100644 index 00000000..0dfc0b04 --- /dev/null +++ b/Aplib.Core/Intent/Actions/QueryAction.cs @@ -0,0 +1,70 @@ +using Aplib.Core.Belief.BeliefSets; +using Aplib.Core.Intent.Tactics; + +namespace Aplib.Core.Intent.Actions +{ + /// + /// Describes an action that can be executed and guarded with a query that stores a result. + /// The result can be used in the effect. + /// + /// The belief set of the agent. + /// The type of the query of the action + public class QueryAction : Action, IQueryable + where TBeliefSet : IBeliefSet + { + /// + /// Gets or sets the effect of the action. + /// + protected readonly new System.Action _effect; + + /// + /// Gets or sets the query of the action. + /// + protected readonly System.Func _query; + + /// + /// Gets or sets the result of the query. + /// + protected TQuery? _storedQueryResult; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Metadata about this action, used to quickly display the action in several contexts. + /// + /// The effect of the action. + /// The query of the action. + public QueryAction + (IMetadata metadata, System.Action effect, System.Func query) + : base(metadata, _ => { }) + { + _effect = effect; + _query = query; + } + + /// + public QueryAction(System.Action effect, System.Func query) + : this(new Metadata(), effect, query) + { + } + + /// + public override void Execute(TBeliefSet beliefSet) => _effect(beliefSet, _storedQueryResult!); + + /// + /// Queries the environment for the queried item and returns whether the query is not null. + /// + /// The belief set of the agent. + /// True if the query is not null; otherwise, false. + public bool Query(TBeliefSet beliefSet) + { + // Query the environment for the guarded item. + _storedQueryResult = _query(beliefSet); + + // Only return true if the query is not null. + return _storedQueryResult is not null; + } + } +} diff --git a/Aplib.Core/Intent/Tactics/AnyOfTactic.cs b/Aplib.Core/Intent/Tactics/AnyOfTactic.cs index 78982dba..1eba20a0 100644 --- a/Aplib.Core/Intent/Tactics/AnyOfTactic.cs +++ b/Aplib.Core/Intent/Tactics/AnyOfTactic.cs @@ -1,4 +1,4 @@ -using Aplib.Core.Belief; +using Aplib.Core.Belief.BeliefSets; using Aplib.Core.Intent.Actions; using System.Collections.Generic; @@ -13,52 +13,59 @@ public class AnyOfTactic : Tactic /// /// Gets or sets the sub-tactics of the tactic. /// - protected LinkedList> _subTactics { get; set; } + protected readonly LinkedList> _subTactics; /// - /// Initializes a new instance of the class with the specified sub-tactics. + /// Initializes a new instance of the class with the specified sub-tactics + /// and an optional guard condition. /// /// /// Metadata about this tactic, used to quickly display the tactic in several contexts. /// - /// The list of sub-tactics. - public AnyOfTactic(Metadata? metadata = null, params ITactic[] subTactics) - : base(metadata) + /// The guard condition. + /// The list of subtactics. + public AnyOfTactic + ( + IMetadata metadata, + System.Func guard, + params ITactic[] subTactics + ) + : base(metadata, guard) => _subTactics = new LinkedList>(subTactics); + + /// + public AnyOfTactic + (System.Func guard, params ITactic[] subTactics) + : this(new Metadata(), guard, subTactics) { - _subTactics = new(); + } - foreach (ITactic tactic in subTactics) - { - _ = _subTactics.AddLast(tactic); - } + /// + public AnyOfTactic(IMetadata metadata, params ITactic[] subTactics) + : this(metadata, _ => true, subTactics) + { } - /// - /// Initializes a new instance of the class with the specified sub-tactics and guard condition. - /// - /// The guard condition. - /// - /// Metadata about this tactic, used to quickly display the tactic in several contexts. - /// - /// The list of sub-tactics. - public AnyOfTactic(System.Func guard, Metadata? metadata = null, params ITactic[] subTactics) - : this(metadata, subTactics) => _guard = guard; + /// + public AnyOfTactic(params ITactic[] subTactics) + : this(new Metadata(), _ => true, subTactics) + { + } /// public override IAction? GetAction(TBeliefSet beliefSet) { + if (!IsActionable(beliefSet)) return null; + List> actions = new(); foreach (ITactic subTactic in _subTactics) { IAction? action = subTactic.GetAction(beliefSet); - if (action is not null) - actions.Add(action); + if (action is not null) actions.Add(action); } - if (actions.Count == 0) - return null; + if (actions.Count == 0) return null; return actions[ThreadSafeRandom.Next(actions.Count)]; } diff --git a/Aplib.Core/Intent/Tactics/FirstOfTactic.cs b/Aplib.Core/Intent/Tactics/FirstOfTactic.cs index 606e8d4a..c9955111 100644 --- a/Aplib.Core/Intent/Tactics/FirstOfTactic.cs +++ b/Aplib.Core/Intent/Tactics/FirstOfTactic.cs @@ -1,5 +1,6 @@ -using Aplib.Core.Belief; +using Aplib.Core.Belief.BeliefSets; using Aplib.Core.Intent.Actions; +using System.Linq; namespace Aplib.Core.Intent.Tactics { @@ -10,38 +11,45 @@ public class FirstOfTactic : AnyOfTactic where TBeliefSet : IBeliefSet { /// - /// Initializes a new instance of the class with the specified sub-tactics. + /// Initializes a new instance of the class with the specified + /// sub-tactics and guard condition. /// /// /// Metadata about this tactic, used to quickly display the tactic in several contexts. /// - /// The list of sub-tactics. - public FirstOfTactic(Metadata? metadata = null, params Tactic[] subTactics) - : base(metadata, subTactics) { } - - /// - /// Initializes a new instance of the class with the specified sub-tactics and guard condition. - /// /// The guard condition. - /// - /// Metadata about this tactic, used to quickly display the tactic in several contexts. - /// - /// The list of sub-tactics. - public FirstOfTactic(System.Func guard, Metadata? metadata = null, params Tactic[] subTactics) - : base(guard, metadata, subTactics) { } + /// The list of subtactics. + public FirstOfTactic + (IMetadata metadata, System.Func guard, params ITactic[] subTactics) + : base(metadata, guard, subTactics) + { + } + + /// + public FirstOfTactic(IMetadata metadata, params ITactic[] subTactics) + : this(metadata, _ => true, subTactics) + { + } + + /// + public FirstOfTactic + (System.Func guard, params ITactic[] subTactics) + : this(new Metadata(), guard, subTactics) + { + } + + /// + public FirstOfTactic(params ITactic[] subTactics) : this(new Metadata(), _ => true, subTactics) { } /// public override IAction? GetAction(TBeliefSet beliefSet) { - foreach (ITactic subTactic in _subTactics) - { - IAction? action = subTactic.GetAction(beliefSet); - - if (action is not null) - return action; - } + if (!IsActionable(beliefSet)) return null; - return null; + return _subTactics + .Select(subTactic => subTactic.GetAction(beliefSet)) + .OfType>() + .FirstOrDefault(); } } } diff --git a/Aplib.Core/Intent/Tactics/ITactic.cs b/Aplib.Core/Intent/Tactics/ITactic.cs index 5175c60c..bcae164b 100644 --- a/Aplib.Core/Intent/Tactics/ITactic.cs +++ b/Aplib.Core/Intent/Tactics/ITactic.cs @@ -1,4 +1,4 @@ -using Aplib.Core.Belief; +using Aplib.Core.Belief.BeliefSets; using Aplib.Core.Intent.Actions; namespace Aplib.Core.Intent.Tactics diff --git a/Aplib.Core/Intent/Tactics/PrimitiveTactic.cs b/Aplib.Core/Intent/Tactics/PrimitiveTactic.cs index f68416ea..9c574c2e 100644 --- a/Aplib.Core/Intent/Tactics/PrimitiveTactic.cs +++ b/Aplib.Core/Intent/Tactics/PrimitiveTactic.cs @@ -1,4 +1,4 @@ -using Aplib.Core.Belief; +using Aplib.Core.Belief.BeliefSets; using Aplib.Core.Intent.Actions; namespace Aplib.Core.Intent.Tactics @@ -16,30 +16,71 @@ public class PrimitiveTactic : Tactic protected readonly IAction _action; /// - /// Initializes a new instance of the class with the specified action. + /// Initializes a new instance of the class with the specified action + /// and guard. /// - /// The action of the primitive tactic. /// /// Metadata about this tactic, used to quickly display the tactic in several contexts. /// - public PrimitiveTactic(IAction action, Metadata? metadata = null) - : base(metadata) => _action = action; + /// The action of the primitive tactic. + /// The guard of the primitive tactic. + public PrimitiveTactic(IMetadata metadata, IAction action, System.Func guard) + : base(metadata, guard) => _action = action; + + /// + public PrimitiveTactic(IAction action, System.Func guard) + : this(new Metadata(), action, guard) + { + } + + /// + public PrimitiveTactic(IMetadata metadata, IAction action) : this(metadata, action, _ => true) { } + + /// + public PrimitiveTactic(IAction action) : this(new Metadata(), action, _ => true) { } /// - /// Initializes a new instance of the class with the specified action and guard. + /// Initializes a new instance of the class with the specified action + /// and guard. /// - /// The action of the primitive tactic. - /// The guard of the primitive tactic. /// /// Metadata about this tactic, used to quickly display the tactic in several contexts. /// - public PrimitiveTactic(IAction action, System.Func guard, Metadata? metadata = null) - : base(guard, metadata) => _action = action; + /// The queryable action of the primitive tactic. + /// The guard of the primitive tactic. + public PrimitiveTactic + (IMetadata metadata, IQueryable queryAction, System.Func guard) + : this + ( + metadata, + action: queryAction, + beliefSet => guard(beliefSet) && queryAction.Query(beliefSet) + ) + { + } - /// - public override IAction? GetAction(TBeliefSet beliefSet) => IsActionable(beliefSet) ? _action : null; + /// + public PrimitiveTactic(IQueryable queryAction, System.Func guard) + : this(new Metadata(), queryAction, guard) + { + } + + /// + public PrimitiveTactic(IMetadata metadata, IQueryable queryAction) + : this(metadata, queryAction, _ => true) + { + } + + /// + public PrimitiveTactic(IQueryable queryAction) + : this(new Metadata(), queryAction, _ => true) + { + } /// - public override bool IsActionable(TBeliefSet beliefSet) => base.IsActionable(beliefSet) && _action.IsActionable(beliefSet); + public override IAction? GetAction(TBeliefSet beliefSet) + => IsActionable(beliefSet) ? _action : null; } } diff --git a/Aplib.Core/Intent/Tactics/Tactic.cs b/Aplib.Core/Intent/Tactics/Tactic.cs index 4bf60ecb..2a471f58 100644 --- a/Aplib.Core/Intent/Tactics/Tactic.cs +++ b/Aplib.Core/Intent/Tactics/Tactic.cs @@ -1,4 +1,5 @@ -using Aplib.Core.Belief; +using Aplib.Core.Belief.BeliefSets; +using Aplib.Core.Desire.Goals; using Aplib.Core.Intent.Actions; namespace Aplib.Core.Intent.Tactics @@ -6,52 +7,55 @@ namespace Aplib.Core.Intent.Tactics /// /// Tactics are the real meat of 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 s, which are executed in a Believe Desire Intent Cycle. + /// a smart combination of s, which are executed in a Belief Desire Intent Cycle. /// /// /// /// The belief set of the agent. - public abstract class Tactic : ITactic + public abstract class Tactic : ITactic, IDocumented where TBeliefSet : IBeliefSet { - /// - /// Gets the metadata of the tactic. - /// - /// - /// This metadata may be useful for debugging or logging. - /// - public Metadata Metadata { get; } - /// /// Gets or sets the guard of the tactic. /// - protected System.Func _guard { get; set; } = _ => true; + protected System.Func _guard; - /// - /// Initializes a new instance of the . - /// - /// - /// Metadata about this tactic, used to quickly display the tactic in several contexts. - /// - protected Tactic(Metadata? metadata) => Metadata = metadata ?? new Metadata(); + /// + public IMetadata Metadata { get; } /// /// Initializes a new instance of the class with a specified guard. /// - /// The guard of the tactic. /// /// Metadata about this tactic, used to quickly display the tactic in several contexts. /// - protected Tactic(System.Func guard, Metadata? metadata = null) + /// The guard of the tactic. + protected Tactic(IMetadata metadata, System.Func guard) { _guard = guard; - Metadata = metadata ?? new Metadata(); + Metadata = metadata; } + /// + protected Tactic(System.Func guard) : this(new Metadata(), guard) { } + + /// + protected Tactic(IMetadata metadata) : this(metadata, _ => true) { } + + /// + protected Tactic() : this(new Metadata(), _ => true) { } + /// public abstract IAction? GetAction(TBeliefSet beliefSet); /// public virtual bool IsActionable(TBeliefSet beliefSet) => _guard(beliefSet); + + /// + /// Implicitly lifts an action into a tactic. + /// + /// + /// The most logically matching tactic, wrapping around . + public static implicit operator Tactic(Action action) => action.Lift(); } } diff --git a/Aplib.Core/LiftingExtensionMethods.cs b/Aplib.Core/LiftingExtensionMethods.cs new file mode 100644 index 00000000..e12a9265 --- /dev/null +++ b/Aplib.Core/LiftingExtensionMethods.cs @@ -0,0 +1,78 @@ +using Aplib.Core.Belief.BeliefSets; +using Aplib.Core.Desire.DesireSets; +using Aplib.Core.Desire.Goals; +using Aplib.Core.Desire.GoalStructures; +using Aplib.Core.Intent.Actions; +using Aplib.Core.Intent.Tactics; + +namespace Aplib.Core +{ + /// + /// Contains extension methods for lifting BDI cycle components into higher-order components. + /// + public static class LiftingExtensionMethods + { + /// + /// Wraps a normal action into a tactic. + /// + /// A primitive tactic, whose guard always returns true. + /// + /// The action which on its own can function as a tactic. Meaning, the tactic consists of just a single action. + /// + /// Optional metadata to be assigned to the tactic. + public static PrimitiveTactic Lift(this IAction action, IMetadata metadata) + where TBeliefSet : IBeliefSet => new(metadata, action: action); + + /// + public static PrimitiveTactic Lift(this IAction action) + where TBeliefSet : IBeliefSet => action.Lift(new Metadata()); + + /// + /// Wraps a queryable action into a tactic. + /// + /// A primitive tactic, whose guard always returns true. + /// + /// The action which on its own can function as a tactic. Meaning, the tactic consists of just a single action. + /// + /// Optional metadata to be assigned to the tactic. + public static PrimitiveTactic Lift(this IQueryable action, IMetadata metadata) + where TBeliefSet : IBeliefSet => new(metadata, queryAction: action); + + + /// + public static PrimitiveTactic Lift(this IQueryable action) + where TBeliefSet : IBeliefSet => action.Lift(new Metadata()); + + /// + /// Wraps a goal into a goal structure. + /// + /// A primitive goal structure. + /// + /// The goal which on its own can function as a goal structure. Meaning, the goal structure consists of just a + /// single goal. + /// + /// Optional metadata to be assigned to the goal structure. + public static PrimitiveGoalStructure Lift(this IGoal goal, IMetadata metadata) + where TBeliefSet : IBeliefSet => new(metadata, goal); + + /// + public static PrimitiveGoalStructure Lift(this IGoal goal) + where TBeliefSet : IBeliefSet => goal.Lift(new Metadata()); + + /// + /// Wraps a goal structure into a desire set. + /// + /// A desire set. + /// + /// The goal structure which on its own can function as a desire set. Meaning, the desire set consists of just + /// a single goal structure. + /// + /// Optional metadata to be assigned to the desire set. + public static DesireSet Lift(this IGoalStructure goalStructure, IMetadata metadata) + where TBeliefSet : IBeliefSet => new(metadata, goalStructure); + + /// + public static DesireSet Lift(this IGoalStructure goalStructure) + where TBeliefSet : IBeliefSet => goalStructure.Lift(new Metadata()); + } +} diff --git a/Aplib.Core/Metadata.cs b/Aplib.Core/Metadata.cs index 07924d8d..9535a010 100644 --- a/Aplib.Core/Metadata.cs +++ b/Aplib.Core/Metadata.cs @@ -5,43 +5,36 @@ namespace Aplib.Core /// /// Data structure to store information about a component which may be useful for debugging or logging. /// - public class Metadata + public class Metadata : IMetadata { - /// - /// Gets the unique identifier of the component. - /// + /// public Guid Id { get; } - /// - /// Gets the name used to display the component during debugging, logging, or general overviews. - /// + /// public string? Name { get; } - /// - /// Gets the description used to describe the component during debugging, logging, or general overviews. - /// + /// public string? Description { get; } /// - /// Store information about a component which may be useful for debugging or logging or general overviews. + /// Store information about a BDI cycle component which may be useful for debugging or logging or general overviews. /// /// The name used to display the component. /// The description used to describe the component. - public Metadata(string? name = null, string? description = null) - { - Id = Guid.NewGuid(); - Name = name; - Description = description; - } + public Metadata(string? name = null, string? description = null) : this(Guid.NewGuid(), name, description) { } /// - /// Store information about a component which may be useful for debugging or logging or general overviews. + /// Store information about a BDI cycle component which may be useful for debugging or logging or general overviews. /// /// This constructor is mainly for testing. /// A unique identifier for the component. /// The name used to display the component. /// The description used to describe the component. internal Metadata(Guid id, string? name = null, string? description = null) - : this(name, description) => Id = id; + { + Id = id; + Name = name; + Description = description; + } } } diff --git a/Aplib.Core/ThreadSafeRandom.cs b/Aplib.Core/ThreadSafeRandom.cs index 0a6d2996..f2506e4a 100644 --- a/Aplib.Core/ThreadSafeRandom.cs +++ b/Aplib.Core/ThreadSafeRandom.cs @@ -8,7 +8,7 @@ internal static class ThreadSafeRandom private static Random? _local; private static readonly Random _global = new(); - private static Random _instance + private static Random Instance { get { @@ -27,8 +27,8 @@ private static Random _instance } } - public static int Next() => _instance.Next(); + public static int Next() => Instance.Next(); - public static int Next(int maxValue) => _instance.Next(maxValue); + public static int Next(int maxValue) => Instance.Next(maxValue); } } diff --git a/Aplib.Extensions/Aplib.Extensions.csproj b/Aplib.Extensions/Aplib.Extensions.csproj new file mode 100644 index 00000000..6358f10e --- /dev/null +++ b/Aplib.Extensions/Aplib.Extensions.csproj @@ -0,0 +1,13 @@ + + + + netstandard2.1 + 9 + enable + + + + + + + diff --git a/Aplib.Extensions/IPathfinder.cs b/Aplib.Extensions/IPathfinder.cs new file mode 100644 index 00000000..8fe10059 --- /dev/null +++ b/Aplib.Extensions/IPathfinder.cs @@ -0,0 +1,39 @@ +using System; + +namespace Aplib.Extensions +{ + /// + /// Defines a pathfinding algorithm used to find a path from a starting point to an end point. + /// + /// The type of the elements in the path. + public interface IPathfinder + { + /// + /// Finds a path from the specified starting point to the specified end point. + /// + /// The type of the elements in the path. + /// The starting point of the path. + /// The end point of the path. + /// A read-only span of elements representing the path from the starting point to the end point. + public ReadOnlySpan FindPath(T begin, T end); + + /// + /// Gets the next step towards the specified end point from the current point in the path. + /// + /// The type of the elements in the path. + /// The current point in the path. + /// The end point of the path. + /// The next step towards the end point. + public T GetNextStep(T current, T end); + + /// + /// Tries to get the next step towards the specified end point from the current point in the path. + /// + /// The type of the elements in the path. + /// The current point in the path. + /// The end point of the path. + /// When this method returns, contains the next step towards the end point if it exists, or the default value of type T if there is no next step. + /// true if the next step towards the end point is obtained successfully; otherwise, false. + public bool TryGetNextStep(T current, T end, out T nextStep); + } +} diff --git a/Aplib.Net.sln b/Aplib.Net.sln index 9fce7b06..8ac0a69e 100644 --- a/Aplib.Net.sln +++ b/Aplib.Net.sln @@ -5,7 +5,9 @@ VisualStudioVersion = 17.8.34525.116 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aplib.Core", "Aplib.Core\Aplib.Core.csproj", "{0ED50CC0-F90F-44D4-830E-2F3BACE040EC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aplib.Core.Tests", "Aplib.Tests\Aplib.Core.Tests.csproj", "{67D4F6C5-758D-489F-A100-8B259B2158D2}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aplib.Extensions", "Aplib.Extensions\Aplib.Extensions.csproj", "{8D5F1FCC-D4DF-4D50-BCC4-EBE52D0D4F5B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aplib.Core.Tests", "Aplib.Core.Tests\Aplib.Core.Tests.csproj", "{8EA09964-F384-48BD-8CD4-0F6867B66BB3}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -17,10 +19,14 @@ Global {0ED50CC0-F90F-44D4-830E-2F3BACE040EC}.Debug|Any CPU.Build.0 = Debug|Any CPU {0ED50CC0-F90F-44D4-830E-2F3BACE040EC}.Release|Any CPU.ActiveCfg = Release|Any CPU {0ED50CC0-F90F-44D4-830E-2F3BACE040EC}.Release|Any CPU.Build.0 = Release|Any CPU - {67D4F6C5-758D-489F-A100-8B259B2158D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {67D4F6C5-758D-489F-A100-8B259B2158D2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {67D4F6C5-758D-489F-A100-8B259B2158D2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {67D4F6C5-758D-489F-A100-8B259B2158D2}.Release|Any CPU.Build.0 = Release|Any CPU + {8D5F1FCC-D4DF-4D50-BCC4-EBE52D0D4F5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D5F1FCC-D4DF-4D50-BCC4-EBE52D0D4F5B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D5F1FCC-D4DF-4D50-BCC4-EBE52D0D4F5B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D5F1FCC-D4DF-4D50-BCC4-EBE52D0D4F5B}.Release|Any CPU.Build.0 = Release|Any CPU + {8EA09964-F384-48BD-8CD4-0F6867B66BB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8EA09964-F384-48BD-8CD4-0F6867B66BB3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8EA09964-F384-48BD-8CD4-0F6867B66BB3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8EA09964-F384-48BD-8CD4-0F6867B66BB3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Aplib.Tests/Belief/BeliefTests.cs b/Aplib.Tests/Belief/BeliefTests.cs deleted file mode 100644 index 52fc049f..00000000 --- a/Aplib.Tests/Belief/BeliefTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -using Aplib.Core.Belief; -using System.Collections.Generic; -using System.Linq; - -namespace Aplib.Core.Tests.Belief; - -/// -/// Describes a set of tests for the class. -/// -public class BeliefTests -{ - /// - /// A constant 'true' method for testing. - /// - /// True. - private static bool AlwaysUpdate() => true; - - /// - /// A constant 'false' method for testing. - /// - /// False. - private static bool NeverUpdate() => false; - - /// - /// Given a Belief instance, - /// When it is assigned to a variable of its observation type, - /// Then it is implicitly converted to its observation type. - /// - [Fact] - public void Belief_AssignedToObservationType_IsCorrectlyImplicitlyConvertedToObservationType() - { - // Arrange - string def = "def"; - Belief belief = new(def, reference => reference); - - // Act - string observation = belief; - - // Assert - Assert.Equal(def, observation); - } - - /// - /// Given a Belief instance with an shouldUpdate condition that is satisfied, - /// When UpdateBelief is called, - /// Then the observation is updated. - /// - [Fact] - public void UpdateBelief_ShouldUpdateConditionIsSatisfied_UpdatesObservation() - { - // Arrange - List list = []; - Belief, int> belief = new(list, reference => reference.Count, AlwaysUpdate); - - // Act - list.Add(69); - belief.UpdateBelief(); - - // Assert - Assert.Equal(list.Count, belief); - } - - /// - /// Given a Belief instance with an shouldUpdate condition that is not satisfied, - /// When UpdateBelief is called, - /// Then the observation is not updated. - /// - [Fact] - public void UpdateBelief_ShouldUpdateConditionIsNotSatisfied_DoesNotUpdateObservation() - { - // Arrange - List list = []; - Belief, int> belief = new(list, reference => reference.Count, NeverUpdate); - - // Act - list.Add(420); - belief.UpdateBelief(); - - // Assert - Assert.NotEqual(list.Count, belief); - } - - /// - /// Given a Belief instance with a reference, - /// When the reference is assigned to and UpdateBelief is called, - /// Then the observation is not updated. - /// - [Fact] - public void UpdateBelief_ReferenceIsAssignedTo_DoesNotUpdateObservation() - { - // Arrange - string def = "def"; - Belief belief = new(def, reference => reference, AlwaysUpdate); - - // Act - def = "abc"; - belief.UpdateBelief(); - - // Assert - Assert.NotEqual(def, belief); - } -} diff --git a/Aplib.Tests/Belief/MemoryBeliefTests.cs b/Aplib.Tests/Belief/MemoryBeliefTests.cs deleted file mode 100644 index 04767912..00000000 --- a/Aplib.Tests/Belief/MemoryBeliefTests.cs +++ /dev/null @@ -1,94 +0,0 @@ -using Aplib.Core.Belief; -using System.Collections.Generic; - -namespace Aplib.Core.Tests.Belief; - -/// -/// Describes a set of tests for the class. -/// -public class MemoryBeliefTests -{ - /// - /// Given a MemoryBelief instance with an observation, - /// When the observation is updated and GetMostRecentMemory is called, - /// Then the last observation is returned. - /// - [Fact] - public void GetMostRecentMemory_WhenObservationIsUpdated_ShouldReturnLastObservation() - { - // Arrange - List list = [1]; - MemoryBelief, int> belief = new(list, reference => reference.Count, 1); - - // Act - list.Add(2); - belief.UpdateBelief(); - - // Assert - Assert.Equal(1, belief.GetMostRecentMemory()); - } - - /// - /// Given a MemoryBelief instance with an observation, - /// When the observation is updated and GetMemoryAt is called with an index, - /// Then the observation at the specified index is returned. - /// - [Fact] - public void GetMemoryAt_WhenObservationIsUpdated_ShouldReturnObservationAtSpecifiedIndex() - { - // Arrange - List list = [1, 2, 3]; - MemoryBelief, int> belief = new(list, reference => reference.Count, 3); - - // Act - list.Add(4); - belief.UpdateBelief(); - list.Add(5); - belief.UpdateBelief(); - - // Assert - Assert.Equal(4, belief.GetMemoryAt(0)); - Assert.Equal(3, belief.GetMemoryAt(1)); - } - - /// - /// Given a MemoryBelief instance with an observation, - /// When asking for an index that is out of bounds, - /// Then the closest element that is in bounds is returned. - /// - [Fact] - public void GetMemoryAt_IndexOutOfBounds_ShouldReturnClosestElement() - { - // Arrange - List list = [1, 2, 3]; - MemoryBelief, int> belief = new(list, reference => reference.Count, 3); - - // Act - list.Add(4); - belief.UpdateBelief(); - - // Assert - Assert.Equal(3, belief.GetMemoryAt(-1, true)); - Assert.Equal(0, belief.GetMemoryAt(5, true)); - } - - /// - /// Given a MemoryBelief instance with an observation, - /// When the observation is updated and GetAllMemories is called, - /// Then all the currently saved observations are returned. - /// - [Fact] - public void GetAllMemories_ReturnsAllMemories() - { - // Arrange - List list = [1, 2, 3]; - MemoryBelief, int> belief = new(list, reference => reference.Count, 3); - - // Act - list.Add(4); - belief.UpdateBelief(); - - // Assert - Assert.Equal([3, 0, 0], belief.GetAllMemories()); - } -} diff --git a/Aplib.Tests/Desire/DesireSetTests.cs b/Aplib.Tests/Desire/DesireSetTests.cs deleted file mode 100644 index 87ac47f6..00000000 --- a/Aplib.Tests/Desire/DesireSetTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Aplib.Core.Belief; -using Aplib.Core.Desire; -using Aplib.Core.Desire.Goals; -using FluentAssertions; -using Moq; - -namespace Aplib.Core.Tests.Desire; - -public class DesireSetTests -{ - /// - /// Given a desire set, - /// When the GetCurrentGoal method is called, - /// Then the desire set should return the current goal of the current goal structure. - /// - [Fact] - public void DesireSet_WhenGetCurrentGoalIsCalled_ReturnsCurrentGoal() - { - // Arrange - IBeliefSet beliefSet = Mock.Of(); - IGoal goal = Mock.Of>(); - Mock> desireSet = new(); - desireSet - .Setup(d => d.GetCurrentGoal(It.IsAny())) - .Returns(goal); - - // Act - IGoal currentGoal = desireSet.Object.GetCurrentGoal(beliefSet); - - // Assert - currentGoal.Should().Be(goal); - } - - /// - /// Given a desire set, - /// When the status is updated, - /// Then the status of the goal structures are updated. - /// - [Fact] - public void DesireSet_WhenStatusUpdated_ShouldUpdateGoalStructuresStatus() - { - // Arrange - Mock> goalStructure = new(); - Mock> desireSet = new(goalStructure.Object); - - // Act - desireSet.Object.UpdateStatus(It.IsAny()); - - // Assert - goalStructure.Verify(g => g.UpdateStatus(It.IsAny()), Times.Once()); - } - - /// - /// Given a desire set, - /// When the status is checked, - /// Then the status should be the same as the main goal structure status. - /// - [Fact] - public void DesireSet_WhenStatusIsChecked_ShouldBeSameAsMainGoal() - { - // Arrange - Mock> goalStructure = new(); - goalStructure.Setup(g => g.Status).Returns(CompletionStatus.Success); - Mock> desireSet = new(goalStructure.Object); - - // Act - CompletionStatus status = desireSet.Object.Status; - CompletionStatus expectedStatus = goalStructure.Object.Status; - - // Assert - status.Should().Be(expectedStatus); - } -} diff --git a/Aplib.Tests/Intent/Actions/ActionTests.cs b/Aplib.Tests/Intent/Actions/ActionTests.cs deleted file mode 100644 index 0c097e11..00000000 --- a/Aplib.Tests/Intent/Actions/ActionTests.cs +++ /dev/null @@ -1,201 +0,0 @@ -using Aplib.Core; -using Aplib.Core.Belief; -using Aplib.Core.Intent.Actions; -using FluentAssertions; -using Moq; - -namespace Aplib.Core.Tests.Intent.Actions; - -/// -/// Describes a set of tests for the class. -/// -public class ActionTests -{ - [Fact] - public void Action_WhenConstructed_ContainsCorrectMetaData() - { - // Arrange - const string name = "Action"; - const string description = "A cheap store where I get all my stuff"; - Metadata metadata = new(name, description); - - // Act - Action action = new(_ => { }, metadata); - - // Assert - action.Should().NotBeNull(); - action.Metadata.Name.Should().Be(name); - action.Metadata.Description.Should().Be(description); - } - - [Fact] - public void Action_WithoutDescription_ContainsCorrectMetaData() - { - // Arrange - const string name = "my action"; - Metadata metadata = new(name); - - // Act - Action action = new(_ => { }, metadata); - - // Assert - action.Should().NotBeNull(); - action.Metadata.Name.Should().Be(name); - action.Metadata.Description.Should().BeNull(); - } - - /// - /// Given a side effect action, - /// When the action is executed, - /// Then the result should not be null. - /// - [Fact] - public void Execute_SideEffects_ReturnsCorrectEffect() - { - // Arrange - string? result = null; - IBeliefSet beliefSet = Mock.Of(); - Action action = new(_ => result = "abc"); - - // Act - action.Execute(beliefSet); - - // Assert - result.Should().Be("abc"); - } - - /// - /// 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 Execute_WithGuard_ShouldInvokeQueryAndStoreResult() - { - // Arrange - int result = 0; - IBeliefSet beliefSet = Mock.Of(); - GuardedAction action = new(guard: _ => 42, effect: (_, query) => result = query); - - // Act - _ = action.IsActionable(beliefSet); - action.Execute(beliefSet); - - // Assert - result.Should().Be(42); - } - - /// - /// Given an action with no query, - /// When checking if the action is actionable, - /// Then the result should always be true. - /// - [Fact] - public void IsActionable_NoQuery_AlwaysTrue() - { - // Arrange - IBeliefSet beliefSet = Mock.Of(); - Action action = new(_ => { }); - - // Act - bool actionable = action.IsActionable(beliefSet); - - // Assert - actionable.Should().BeTrue(); - } - - /// - /// Given an action with a false bool guard, - /// When checking if the action is actionable, - /// Then the result should be true. - /// - [Fact] - public void IsActionable_QueryIsFalse_IsActionable() - { - // Arrange - IBeliefSet beliefSet = Mock.Of(); - GuardedAction action = new(guard: _ => false, effect: (_, _) => { }); - - // Act - bool actionable = action.IsActionable(beliefSet); - - // Assert - actionable.Should().BeTrue(); - } - - /// - /// 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 IsActionable_QueryIsNotNull_IsActionable() - { - // Arrange - IBeliefSet beliefSet = Mock.Of(); - GuardedAction action = new(guard: _ => 10, effect: (_, _) => { }); - - // Act - bool actionable = action.IsActionable(beliefSet); - - // Assert - actionable.Should().BeTrue(); - } - - /// - /// Given an action with a null object guard, - /// When checking if the action is actionable, - /// Then the result should be false. - /// - [Fact] - public void IsActionable_QueryIsNull_IsNotActionable() - { - // Arrange - IBeliefSet beliefSet = Mock.Of(); - GuardedAction action = new(guard: _ => null!, effect: (_, _) => { }); - - // Act - bool actionable = action.IsActionable(beliefSet); - - // Assert - actionable.Should().BeFalse(); - } - - /// - /// Given an action with a false query, - /// When checking if the action is actionable, - /// Then the result should be false. - /// - [Fact] - public void IsActionable_QueryWithFalse_ReturnsFalse() - { - // Arrange - IBeliefSet beliefSet = Mock.Of(); - Action action = new(_ => { }, _ => false); - - // Act - bool actionable = action.IsActionable(beliefSet); - - // Assert - actionable.Should().BeFalse(); - } - - /// - /// Given an action with a true query, - /// When checking if the action is actionable, - /// Then the result should be true. - /// - [Fact] - public void IsActionable_QueryWithTrue_ReturnsTrue() - { - // Arrange - IBeliefSet beliefSet = Mock.Of(); - Action action = new(_ => { }, _ => true); - - // Act - bool actionable = action.IsActionable(beliefSet); - - // Assert - actionable.Should().BeTrue(); - } -} diff --git a/Aplib.Tests/Intent/Tactics/TacticTests.cs b/Aplib.Tests/Intent/Tactics/TacticTests.cs deleted file mode 100644 index 6f0eb8ba..00000000 --- a/Aplib.Tests/Intent/Tactics/TacticTests.cs +++ /dev/null @@ -1,171 +0,0 @@ -using Aplib.Core.Belief; -using Aplib.Core.Intent.Actions; -using Aplib.Core.Intent.Tactics; -using FluentAssertions; -using Moq; - -namespace Aplib.Core.Tests.Intent.Tactics; - -public class TacticTests -{ - /// - /// Given a tactic and an action, - /// When the guard of the tactic returns true, - /// Then an action should be selected. - /// - [Fact] - public void PrimitveTacticExecute_WhenGuardReturnsTrue_ActionIsSelected() - { - // Arrange - Mock> action = new(); - action.Setup(a => a.IsActionable(It.IsAny())).Returns(true); - PrimitiveTactic tactic = new(action.Object, guard: _ => true); - - // Act - IAction selectedAction = tactic.GetAction(It.IsAny())!; - - // Assert - selectedAction.Should().NotBeNull(); - } - - /// - /// Given a parent of type with two subtactics, - /// When getting the next tactic, - /// Then the result should be the action of an enabled tactic. - /// - [Fact] - public void AnyOfTacticGetAction_WhenASubtacticIsEnabled_ReturnsActionOfEnabledSubtactic() - { - // Arrange - Action action1 = new(_ => { }); - Action action2 = new(_ => { }); - PrimitiveTactic tactic1 = new(action1, _ => true); - PrimitiveTactic tactic2 = new(action2, _ => false); - AnyOfTactic parentTactic = new(null, tactic1, tactic2); - - // Act - IAction? selectedAction = parentTactic.GetAction(It.IsAny()); - - // Assert - selectedAction.Should().Be(action1); - } - - /// - /// Given a parent of type with two subtactics, - /// When both subtactic guards are true, - /// Then the result should be the first subtactic. - /// - [Fact] - public void FirstOfTacticGetAction_WhenBothSubtacticsEnabled_ReturnsFirstSubtactic() - { - // Arrange - Action action1 = new(_ => { }); - Action action2 = new(_ => { }); - PrimitiveTactic tactic1 = new(action1, _ => true); - PrimitiveTactic tactic2 = new(action2, _ => true); - FirstOfTactic parentTactic = new(null, tactic1, tactic2); - - // Act - IAction? selectedAction = parentTactic.GetAction(It.IsAny()); - - // Assert - selectedAction.Should().Be(action1); - } - - /// - /// Given a parent of type with two subtactics, - /// When the first subtactic guard is false and the second is true, - /// Then the result should be the second subtactic. - /// - [Fact] - public void FirstOfTacticGetAction_WhenFirstSubtacticIsDisabled_ReturnsSecondSubtactic() - { - // Arrange - Action action1 = new(_ => { }); - Action action2 = new(_ => { }); - PrimitiveTactic tactic1 = new(action1, _ => false); - PrimitiveTactic tactic2 = new(action2, _ => true); - FirstOfTactic parentTactic = new(null, tactic1, tactic2); - - // Act - IAction? selectedAction = parentTactic.GetAction(It.IsAny()); - - // Assert - selectedAction.Should().Be(action2); - } - - /// - /// Given a primitive tactic with an actionable action, - /// When calling GetAction, - /// Then the result should be the action of the tactic. - /// - [Fact] - public void PrimitiveTacticGetAction_WhenTacticTypeIsPrimitiveAndActionIsActionable_ReturnsAction() - { - // Arrange - Action action = new(_ => { }, _ => true); - PrimitiveTactic tactic = new(action); - - // Act - IAction? enabledAction = tactic.GetAction(It.IsAny()); - - // Assert - enabledAction.Should().Be(action); - } - - /// - /// Given a primitive tactic with a non-actionable action, - /// When calling GetAction, - /// Then the result should be null. - /// - [Fact] - public void PrimitiveTactic_WhenTacticTypeIsPrimitiveAndActionIsNotActionable_ReturnsNoAction() - { - // Arrange - Action action = new(_ => { }, _ => false); - PrimitiveTactic tactic = new(action); - - // Act - IAction? enabledAction = tactic.GetAction(It.IsAny()); - - // Assert - enabledAction.Should().Be(null); - } - - /// - /// 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 PrimitiveTacticIsActionable_WhenGuardReturnsFalse_ReturnsFalse() - { - // Arrange - PrimitiveTactic tactic = new(It.IsAny>(), _ => false); - - // Act - bool isActionable = tactic.IsActionable(It.IsAny()); - - // Assert - isActionable.Should().BeFalse(); - } - - /// - /// Given a tactic with a guard that returns true and an actionable action, - /// When checking if the tactic is actionable, - /// Then the result should be true. - /// - [Fact] - public void PrimitiveTacticIsActionable_WhenGuardReturnsTrueAndActionIsActionable_ReturnsTrue() - { - // Arrange - Action action = new(_ => { }, _ => true); - PrimitiveTactic tactic = new(action, _ => true); - - // Act - bool isActionable = tactic.IsActionable(It.IsAny()); - - // Assert - isActionable.Should().BeTrue(); - } -}