Last active
August 9, 2022 19:56
-
-
Save ambrosiogabe/ba6bd0fa80588c2fd2ca26d828a09ac2 to your computer and use it in GitHub Desktop.
C# Crafting Recipe Comparison
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Results from Benchmark.NET | |
| // | Method | Mean | Error | StdDev | Min | Max | | |
| // |--------------- |---------:|--------:|--------:|---------:|---------:| | |
| // | FindRecipe | 304.3 ns | 0.76 ns | 0.71 ns | 303.2 ns | 305.8 ns | | |
| // | FastFindRecipe | 125.6 ns | 0.27 ns | 0.25 ns | 125.3 ns | 126.1 ns | | |
| using BenchmarkDotNet.Attributes; | |
| using BenchmarkDotNet.Running; | |
| using System.Diagnostics.CodeAnalysis; | |
| public interface ICraftingRecipe | |
| { | |
| IList<(int itemId, int qty)> InputMaterials { get; } | |
| int ProducedItemId { get; } | |
| int ProducedQuantity { get; } | |
| } | |
| public class CraftingComparer : IEqualityComparer<CraftingRecipe> | |
| { | |
| public bool Equals(CraftingRecipe? x, CraftingRecipe? y) | |
| { | |
| if (x == null || y == null) return false; | |
| return x.InputMaterials.SequenceEqual(y.InputMaterials); | |
| } | |
| public int GetHashCode([DisallowNull] CraftingRecipe obj) | |
| { | |
| if (obj.InputMaterials.Count == 0) | |
| { | |
| return -1; | |
| } | |
| int hash = obj.InputMaterials[0].itemId.GetHashCode(); | |
| for (int i = 1; i < obj.InputMaterials.Count; i++) | |
| { | |
| hash ^= obj.InputMaterials[i].itemId.GetHashCode(); | |
| } | |
| return hash; | |
| } | |
| } | |
| public class CraftingRecipe : ICraftingRecipe | |
| { | |
| public IList<(int itemId, int qty)> InputMaterials { get; set; } = new List<(int itemId, int qty)> { }; | |
| public int ProducedItemId { get; set; } | |
| public int ProducedQuantity { get; set; } | |
| } | |
| public static class CraftingRecipes | |
| { | |
| private static readonly List<ICraftingRecipe> RecipeList = new List<ICraftingRecipe>(); | |
| private static readonly Dictionary<int, List<ICraftingRecipe>> RecipeIndex = new Dictionary<int, List<ICraftingRecipe>>(); | |
| public static IList<ICraftingRecipe> Recipes => RecipeList; | |
| public static ICraftingRecipe? Find(IList<(int itemId, int qty)> materials) | |
| => RecipeIndex.TryGetValue(CalcRecipeKey(materials), out var bucket) ? | |
| bucket != null ? bucket.FirstOrDefault(recipe => Match(recipe, materials)) : null : null; | |
| // No sense using interfaces, there's only ever going to be one implementation of a CraftingRecipe | |
| // in the game. Also, no sense obfuscating how we retrieve the values. A simple hash set should | |
| // do the job | |
| private static readonly HashSet<CraftingRecipe> FastList = new HashSet<CraftingRecipe>(new CraftingComparer()); | |
| public static CraftingRecipe? FastFind(CraftingRecipe r) => FastList.TryGetValue(r, out var res) ? res : null; | |
| public static void RecipeAdd(CraftingRecipe recipe) | |
| { | |
| if (Find(recipe.InputMaterials) == null) | |
| { | |
| RecipeList.Add(recipe); | |
| var key = CalcRecipeKey(recipe.InputMaterials); | |
| if (RecipeIndex.TryGetValue(key, out var bucket)) | |
| bucket.Add(recipe); | |
| else | |
| RecipeIndex.Add(key, new List<ICraftingRecipe> { recipe }); | |
| } | |
| if (!FastList.Contains(recipe)) | |
| { | |
| FastList.Add(recipe); | |
| } | |
| } | |
| private static int CalcRecipeKey(IList<(int itemId, int qty)> materials) | |
| => materials?.Select(x => x.itemId).Prepend(int.MaxValue).Min() ?? int.MaxValue; | |
| private static bool Match(ICraftingRecipe recipe, IList<(int itemId, int qty)> materials) | |
| => materials.SequenceEqual(recipe.InputMaterials); | |
| } | |
| public class Program | |
| { | |
| public static void Main(string[] args) | |
| { | |
| BenchmarkRunner.Run<BenchmarkAlgorithm>(); | |
| } | |
| } | |
| [MinColumn, MaxColumn] | |
| public class BenchmarkAlgorithm | |
| { | |
| public CraftingRecipe RecipeToFind; | |
| private static void InitRecipes(CraftingRecipe recipeToFind, Random rand) | |
| { | |
| for (int i = 0; i < 1_000; i++) | |
| { | |
| CraftingRecipe recipe = new CraftingRecipe(); | |
| recipe.InputMaterials.Add((rand.Next(1000), 1)); | |
| recipe.InputMaterials.Add((rand.Next(1000), 1)); | |
| recipe.InputMaterials.Add((rand.Next(1000), 1)); | |
| recipe.InputMaterials.Add((0, 1)); | |
| recipe.InputMaterials.Add((0, 1)); | |
| recipe.InputMaterials.Add((0, 1)); | |
| recipe.InputMaterials.Add((rand.Next(1000), 1)); | |
| recipe.InputMaterials.Add((rand.Next(1000), 1)); | |
| recipe.InputMaterials.Add((rand.Next(1000), 1)); | |
| CraftingRecipes.RecipeAdd(recipe); | |
| } | |
| CraftingRecipes.RecipeAdd(recipeToFind); | |
| } | |
| [GlobalSetup] | |
| public void Setup() | |
| { | |
| Random rand = new Random(); | |
| CraftingRecipe recipeToFind = new CraftingRecipe(); | |
| recipeToFind.InputMaterials.Add((rand.Next(1000), 1)); | |
| recipeToFind.InputMaterials.Add((rand.Next(1000), 1)); | |
| recipeToFind.InputMaterials.Add((rand.Next(1000), 1)); | |
| recipeToFind.InputMaterials.Add((3, 1)); | |
| recipeToFind.InputMaterials.Add((4, 1)); | |
| recipeToFind.InputMaterials.Add((5, 1)); | |
| recipeToFind.InputMaterials.Add((rand.Next(1000), 1)); | |
| recipeToFind.InputMaterials.Add((rand.Next(1000), 1)); | |
| recipeToFind.InputMaterials.Add((rand.Next(1000), 1)); | |
| InitRecipes(recipeToFind, rand); | |
| RecipeToFind = new CraftingRecipe(); | |
| for (int i = 0; i < recipeToFind.InputMaterials.Count; i++) | |
| { | |
| RecipeToFind.InputMaterials.Add((recipeToFind.InputMaterials[i].itemId, recipeToFind.InputMaterials[i].qty)); | |
| } | |
| ICraftingRecipe? res = CraftingRecipes.Find(RecipeToFind.InputMaterials); | |
| if (res == null) | |
| { | |
| throw new Exception("Failed to find the recipe"); | |
| } | |
| ICraftingRecipe? res2 = CraftingRecipes.FastFind(RecipeToFind); | |
| if (res2 == null) | |
| { | |
| throw new Exception("Failed to find the recipe"); | |
| } | |
| } | |
| [Benchmark] | |
| public void FindRecipe() | |
| { | |
| // NOTE: Skip error checking here because that would alter the performance | |
| // and we do error checking when we set it up to make sure it will | |
| // run appropriately | |
| CraftingRecipes.Find(RecipeToFind.InputMaterials); | |
| } | |
| [Benchmark] | |
| public void FastFindRecipe() | |
| { | |
| // NOTE: Skip error checking here because that would alter the performance | |
| // and we do error checking when we set it up to make sure it will | |
| // run appropriately | |
| CraftingRecipes.FastFind(RecipeToFind); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment