Seeing if a certain grid configuration matches a certain recipe is simple if you encode the 3x3 grid as a string and use a regular expression match. Speeding up the look up is a different matter, which I'll talk about in the end. Read on for more information.
Step 1) Encode grid as String
Simply give a char id to each type of cell and concatenate everything side by side in this order:
123
456 => 123456789
789
And as a more concrete example, consider the stick recipe, where W stands for wood and E is an empty cell (you could simply use an empty char ' '):
EEE
WEE => EEEWEEWEE
WEE
Step 2) Match Recipe using Regular Expression (or String.Contains with a bit of processing on the data)
Continuing from the example above, even if we move the formation around, there's still a pattern in the string (WEEW padded by E on both sides):
EEW
EEW => EEWEEWEEE
EEE
So no matter where you move the stick around, it will still match the following regular expression: /^E*WEEWE*$/
Regular expressions also let you perform the conditional behavior you mentioned. For instance (made up recipe), if you wanted a pickaxe made of iron or stone to give the same result, i.e:
III SSS
EWE or EWE
EWE EWE
You could combine both into the regular expression: /^(III)|(SSS)EWEEWE$/
Horizontal flips can also be added just as easily (using the | operator too).
Edit: Anyway, the regex part is not strictly necessary. It's just one way to encapsulate the problem in a single expression But for the variable location problem you could just as well trim the grid string of any padding spaces (or E's in this example) and do a String.Contains(). And for the multiple ingredient problem or the mirrored recipes, you could just handle all of them as multiple (i.e. separate) recipes with the same output.
Step 3) Speeding up Lookup
As for reducing the search, you will need to create some data structure to group recipes together and help with lookup. Treating the grid as string has some advantages here too:
You could define the "length" of a recipe as being the distance between the first non-empty character and the last non-empty character. A simple Trim().Length()
would give you this information. Recipes could be grouped by length and stored in a dictionary.
or
An alternative definition of "length" could be the number of non-empty characters. Nothing else changes. You could group recipes by this criteria too.
If point number 1 is not enough, recipes may also be further grouped by the type of the first ingredient that appears in the recipe. This would be as simple as doing Trim().CharAt(0)
(and guarding against Trim resulting an empty string).
So for instance you would store recipes in a:
Dictionary<int, Dictionary<char, List<string>>> _recipes;
And perform the lookup as something like:
// A string encode of your current grid configuration
string grid;
// Get length and first char in our grid
string trim = grid.Trim();
int length = trim.Length();
char firstChar = length==0 ? ' ' : trim[0];
foreach(string recipe in _recipes[length][firstChar])
{
// Check for a match with the recipe
if(Regex.Match(grid, recipe))
{
// We found a matching recipe, do something with it
}
}