I'm writing a web page to keep the score for a card game. Getting the players score so far would be easy, but there is a twist. During any round the players score can be reset to zero at the start of the round. I don't want to change the score for any previous rounds, so I only want to get a sum of the rounds after (and including) the reset. A player could potentially have their score reset multiple times in a game, or not at all.
I can get the correct score by a multiple stage process of finding the last (if any) score reset, and summing all hands after that (or all hands in no resets) - see PlayerGame.GetPlayerScore.
I'm still trying to get my head around the more intricate ways of doing things with LINQ, and I was wondering if there was a way to do this using a single LINQ statement?
Minimal code:
class Program
{
static void Main(string[] args)
{
PlayerGame playerGame = new PlayerGame();
playerGame.PlayerHands = new List<PlayerHand>
{
new PlayerHand { Round = 1, Score = 10 },
new PlayerHand { Round = 2, Score = 20 },
new PlayerHand { Round = 3, Score = 30 },
new PlayerHand { Round = 4, Score = 40, Reset = true },
new PlayerHand { Round = 5, Score = 50 },
new PlayerHand { Round = 6, Score = 60 }
};
Console.WriteLine($"Players score was {playerGame.GetPlayerScore()}");
Console.ReadLine();
}
}
class PlayerHand
{
public int Round { get; set; }
public int Score { get; set; }
public bool Reset { get; set; } = false;
}
class PlayerGame
{
public List<PlayerHand> PlayerHands { get; set; }
public PlayerGame()
{
PlayerHands = new List<PlayerHand> { };
}
public int GetPlayerScore()
{
// Can all this be simplified to a single LINQ statement?
var ResetIndex = PlayerHands.OrderBy(t => t.Round).LastOrDefault(t => t.Reset == true);
if (ResetIndex != null)
{
return PlayerHands.Where(t => t.Round >= ResetIndex.Round).Sum(t => t.Score);
}
else
{
return PlayerHands.Sum(t => t.Score);
}
}
}
https://dotnetfiddle.net/s5rSqJ
As presented, the players score should be 150. I.e. the score gets reset at the start of Round 4, so the total score is the sum of Rounds 4, 5, and 6.
In a worksheet, tap the first empty cell after a range of cells that has numbers, or tap and drag to select the range of cells you want to calculate. Tap AutoSum. Tap Sum.
The sum() function returns the sum of an iterable. Sum() takes a list (iterable) and returns the sum of the numbers within the list.
Summarizing a few points,
So, we can come up with an implementation that is O(1) space (in-place, no allocations) and O(n) time (linear, less than the size of the list when there's a reset).
Using MoreLinq
var score = hands.ReverseInPlace().TakeUntil(x => x.Reset).Sum(x => x.Score);
Where ReverseInPlace()
iterates in reverse order in place, and MoreEnumerable.TakeUntil() takes up to and including the round that has a true value for Reset
or end of sequence.
ReverseInPlace
would be an extension method (you could generalize to IEnumerable<>
if you wanted).
public static class ListExtensions
{
public static IEnumerable<T> ReverseInPlace<T>(this IList<T> source)
{
// add guard checks here, then do...
for (int i=source.Length-1; i != -1; --i)
yield return source[i];
}
}
Not using MoreLinq
You could create a TakeInReverseUntil
:
public static IEnumerable<T> TakeInReverseUntil<T>(this IList<T> source, Func<T, bool> predicate)
{
// add guard checks here, then do...
for (int i=source.Length-1; i != -1; --i)
{
yield return source[i];
if (predicate(source[i]) yield break;
}
}
giving you the simplified call
var score = hands.TakeInReverseUntil(x => x.Reset).Sum(x => x.Score);
NOTE: Enumerable.Reverse() allocates a buffer, so is O(n) space, and is why I rolled my own ReverseInPlace
instead for this answer.
The best way I can see here is to simply change your check for the last reset round a bit and combine both statements:
public int GetPlayerScore()
{
// selects the highest Round if Reset == true or 1 by default
var lastResetRound = PlayerHands.Max(hand => hand.Reset ? hand.Round : 1);
return PlayerHands.Where(t => t.Round >= lastResetRound.Round).Sum(t => t.Score);
// or all toghether like this:
return PlayerHands.Where(t => t.Round >= PlayerHands.Max(hand => hand.Reset ? hand.Round : 1)).Sum(t => t.Score);
}
If you used MoreLinq's TakeUntil()
then you could do something like:
PlayerHands
.OrderByDescending(x => x.Round)
.TakeUntil(x => x.Reset)
.Sum(x => x.Score);
Edit: formatting & simplified bool conditional per @PrasadTelkikar
I like the morelinq TakeUntil
most which others have shown, missing it in the standard library.
I've tried to do it without creating new extension methods(cheating) and without morelinq. Following works, but is not as readable and also requires an ordered list(which is the case acc. to your comments).
return PlayerHands
.TakeLast(PlayerHands.Count + 1 - (PlayerHands.FindLast(x => x.Reset)?.Round ?? 1))
.Sum(x => x.Score);
Maybe someone finds a way to simplify the count-calculation.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With