diff --git a/Archipelago.Gifting.Net/Archipelago.Gifting.Net.Tests/UnitTests/CloseTraitParserTests.cs b/Archipelago.Gifting.Net/Archipelago.Gifting.Net.Tests/UnitTests/CloseTraitParserTests.cs index a69bb28..5b4be54 100644 --- a/Archipelago.Gifting.Net/Archipelago.Gifting.Net.Tests/UnitTests/CloseTraitParserTests.cs +++ b/Archipelago.Gifting.Net/Archipelago.Gifting.Net.Tests/UnitTests/CloseTraitParserTests.cs @@ -115,5 +115,81 @@ public void TestGoodClosest() matches.Count.Should().Be(1); matches[0].Should().Be(2); } + + [Test] + public void TestTriangularInequalityWithBaseDistance() + { + BKTreeCloseTraitParser closeTraitParser = new(); + Random random = new(); + + double RandomWeight() + { + switch (random.Next(0, 5)) + { + case <2: + return random.NextDouble(); + case 3: + return 0; + default: + return random.NextDouble() * 9 + 1; + } + } + + for (int i = 0; i < 20; i++) + { + closeTraitParser.RegisterAvailableGift(i, new[] + { + new GiftTrait("a", RandomWeight(), RandomWeight()) + }); + } + for (int i = 20; i < 40; i++) + { + closeTraitParser.RegisterAvailableGift(i, new[] + { + new GiftTrait("b", RandomWeight(), RandomWeight()) + }); + } + for (int i = 40; i < 60; i++) + { + closeTraitParser.RegisterAvailableGift(i, new[] + { + new GiftTrait("c", RandomWeight(), RandomWeight()) + }); + } + for (int i = 60; i < 80; i++) + { + closeTraitParser.RegisterAvailableGift(i, new[] + { + new GiftTrait("a", RandomWeight(), RandomWeight()), + new GiftTrait("b", RandomWeight(), RandomWeight()) + }); + } + for (int i = 80; i < 100; i++) + { + closeTraitParser.RegisterAvailableGift(i, new[] + { + new GiftTrait("b", RandomWeight(), RandomWeight()), + new GiftTrait("c", RandomWeight(), RandomWeight()) + }); + } + for (int i = 100; i < 120; i++) + { + closeTraitParser.RegisterAvailableGift(i, new[] + { + new GiftTrait("a", RandomWeight(), RandomWeight()), + new GiftTrait("c", RandomWeight(), RandomWeight()) + }); + } + for (int i = 120; i < 140; i++) + { + closeTraitParser.RegisterAvailableGift(i, new[] + { + new GiftTrait("a", RandomWeight(), RandomWeight()), + new GiftTrait("b", RandomWeight(), RandomWeight()), + new GiftTrait("c", RandomWeight(), RandomWeight()) + }); + } + closeTraitParser.CheckConsistency(); + } } } diff --git a/Archipelago.Gifting.Net/Archipelago.Gifting.Net/Utilities/CloseTraitParser/BKTreeCloseTraitParser.cs b/Archipelago.Gifting.Net/Archipelago.Gifting.Net/Utilities/CloseTraitParser/BKTreeCloseTraitParser.cs index f11fc43..1ebbb15 100644 --- a/Archipelago.Gifting.Net/Archipelago.Gifting.Net/Utilities/CloseTraitParser/BKTreeCloseTraitParser.cs +++ b/Archipelago.Gifting.Net/Archipelago.Gifting.Net/Utilities/CloseTraitParser/BKTreeCloseTraitParser.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Archipelago.Gifting.Net.Traits; using Archipelago.Gifting.Net.Versioning.Gifts.Current; namespace Archipelago.Gifting.Net.Utilities.CloseTraitParser @@ -137,5 +136,57 @@ public List FindClosestAvailableGift(GiftTrait[] giftTraits) FindClosestAvailableGift(giftTraits, ref bestDistance, ref closestItems); return closestItems; } + + public void CheckConsistency() + { + List, GiftTrait[]>> allBKTrees = + new List, GiftTrait[]>>(); + List> toTreatBKTrees = new List> { this }; + for (int i = 0; i < toTreatBKTrees.Count; i++) + { + List traits = new List(); + foreach (KeyValuePair> keyValuePair in toTreatBKTrees[i]._traits) + { + traits.Add(new GiftTrait(keyValuePair.Key, keyValuePair.Value.Item1, keyValuePair.Value.Item2)); + } + + allBKTrees.Add(new Tuple, GiftTrait[]>(toTreatBKTrees[i], traits.ToArray())); + toTreatBKTrees.AddRange(toTreatBKTrees[i]._children.Values); + } + + foreach (Tuple, GiftTrait[]> BKTree1 in allBKTrees) + { + GiftTrait[] traits1 = BKTree1.Item2; + foreach (Tuple, GiftTrait[]> BKTree2 in allBKTrees) + { + BKTreeCloseTraitParser tree2 = BKTree2.Item1; + GiftTrait[] traits2 = BKTree2.Item2; + + foreach (Tuple, GiftTrait[]> BKTree3 in allBKTrees) + { + BKTreeCloseTraitParser tree3 = BKTree3.Item1; + double d1 = _distance(traits1, tree2._traits, out bool _); + double d2 = _distance(traits2, tree3._traits, out bool _); + double d3 = _distance(traits1, tree3._traits, out bool _); + + if (d1 + d2 - d3 < -0.00001) + // Triangular inequality was violated, margin is smaller than in FindClosestAvailableGift + { + GiftTrait[] traits3 = BKTree3.Item2; + Exception exception = new Exception("Triangular inequalities were violated, " + + "d(traits1, traits2) + d(traits2, traits3) > d(traits1, traits3).\n" + + "Check this exception's data for more details."); + exception.Data.Add("traits1", traits1); + exception.Data.Add("traits2", traits2); + exception.Data.Add("traits3", traits3); + exception.Data.Add("d(traits1, traits2)", d1); + exception.Data.Add("d(traits2, traits3)", d2); + exception.Data.Add("d(traits1, traits3)", d3); + throw exception; + } + } + } + } + } } -} +} \ No newline at end of file diff --git a/Archipelago.Gifting.Net/Archipelago.Gifting.Net/Utilities/CloseTraitParser/ICloseTraitParser.cs b/Archipelago.Gifting.Net/Archipelago.Gifting.Net/Utilities/CloseTraitParser/ICloseTraitParser.cs index 20a146b..fcb4ac1 100644 --- a/Archipelago.Gifting.Net/Archipelago.Gifting.Net/Utilities/CloseTraitParser/ICloseTraitParser.cs +++ b/Archipelago.Gifting.Net/Archipelago.Gifting.Net/Utilities/CloseTraitParser/ICloseTraitParser.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using Archipelago.Gifting.Net.Traits; using Archipelago.Gifting.Net.Versioning.Gifts.Current; namespace Archipelago.Gifting.Net.Utilities.CloseTraitParser diff --git a/README.md b/README.md index 1192481..ae014a2 100644 --- a/README.md +++ b/README.md @@ -253,7 +253,9 @@ If you aren't pleased by the closeness algorithm, you may provide your own as an ```cs double Distance(GiftTrait[] giftTraits, Dictionary> traits, out bool isCompatible); ``` -For this method, all the traits of the registered gift with the same name have been added together for performance reasons +For this method, all the traits of the registered gift with the same name have been added together for performance reasons. +Keep in mind that this function should follow the triangle inequality. +You can run CheckConsistency to test the consistency of your distance function (but do not test it each time you initialize your parser) ## Rejecting a Gift