MasterMind scoring algorithm in C# using LINQ
I'm looking for an elegant way to compute the score of a guess in the MasterMind game in C#, preferably using LINQ.
In MasterMind, the codemaker generates a secret code of 4 digits using the digits 1 through 6. A digit may be used more than once. As an example, the secret code is:
int[] secret = { 1, 2, 3, 1 };
The codebreaker tries to break the secret code by presenting a guess. In this example, the guess is:
int[] guess = { 1, 1, 2, 2 };
(Both code开发者_如何学C and guess are now stored in an array, but other collection types are okay too).
The codemaker then "scores" this guess by announcing the number of "blacks" and "whites". A black is awarded for each digit from the guess which is correct in both value and position. A white is awarded for each correct digit placed in the wrong position. In this example, the score is 1 black (for the "1" in position 1) and 2 whites (for the "1" and "2" in positions 2 and 3).
Back to the question: I'm looking for an elegant way to compute the score of a guess in C#, preferably using LINQ. So far, I've come up with a statement that computes the number of blacks:
int blacks = new int[] { 0, 1, 2, 3 }.Count(i => (guess[i] == secret[i]));
I was going to proceed along the lines that the number of whites is the total number of matches (3) minus the number of blacks. So I tried:
int whites = guess.Intersect(secret).Count() - blacks;
But, alas, IEnumerable.Intersect() produces { 1, 2 } instead of { 1, 1, 2 }, because it looks at distinct digits only. So it computes whites = 1 instead of 2.
I cannot come up with another way of computing "whites", except from using "C" style nested loops. Can you? Preferably using LINQ - I like the way an algorithm can be expressed in code using LINQ. Execution speed is not really an issue.
var black = guess
.Zip(secret, (g, s) => g == s)
.Count(z => z);
var white = guess
.Intersect(secret)
.Sum(c =>
System.Math.Min(
secret.Count(x => x == c),
guess.Count(x => x == c))) - black;
Given:
int[] secret = { 1, 2, 3, 1 };
int[] guess = { 1, 1, 2, 2 };
Then:
black == 1 && white == 2
Here's one way (assuming I've understood the problem correctly):
Find the black score - This is easy enough; it's simply a matter of zipping the sequences up and counting the number of corresponding elements that match.
Find the number of "common elements" between both sequences - This must be the sum of the white and black scores.
Find the white score - Simply the difference between 2. and 1.
// There must be a nicer way of doing this bit
int blackPlusWhite = secret.GroupBy(sNum => sNum)
.Join(guess.GroupBy(gNum => gNum),
g => g.Key,
g => g.Key,
(g1, g2) => Math.Min(g1.Count(), g2.Count()))
.Sum();
int black = guess.Zip(secret, (gNum, sNum) => gNum == sNum)
.Count(correct => correct);
int white = blackPlusWhite - black;
EDIT: Mixed up black and white.
EDIT: (The OP is not on .NET 4) In .NET 3.5, you can calculate black with:
int black = Enumerable.Range(0, secret.Count)
.Count(i => secret[i] == guess[i]);
Ani's answer is good. Here's a nicer (clearer) way to do that grouping and joining.
ILookup<int, int> guessLookup = guess.ToLookup(i => i);
int blackPlusWhite
(
from secretNumber in secret.GroupBy(i => i)
let secretCount = secretNumber.Count()
let guessCount = guessLookup[secretNumber.Key].Count()
select Math.Min(secretCount, guessCount)
).Sum()
int black = Enumerable.Range(0, secret.Count).Count(i => guess[i] == secret[i]);
int white = blackPlusWhite - black;
精彩评论