Skip to content

Instantly share code, notes, and snippets.

@ambrosiogabe
Last active August 9, 2022 19:56
Show Gist options
  • Select an option

  • Save ambrosiogabe/ba6bd0fa80588c2fd2ca26d828a09ac2 to your computer and use it in GitHub Desktop.

Select an option

Save ambrosiogabe/ba6bd0fa80588c2fd2ca26d828a09ac2 to your computer and use it in GitHub Desktop.
C# Crafting Recipe Comparison
// 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