Day 7: Camel Cards
This challenge was another fun one, and speaking to friends a lot of people went down a similar route, either that or there was an abundance of if/else if (I wanted to avoid this), and leant heavily on LINQ.
Brief
Solution
Breakdown
Comparer Class
Final Step
TLDR of Problem
Part 1:
Given 'n' number of Poker Hands, rank the hands based on their Poker hand rankings, e.g:
Five-of-a-Kind
Four-of-a-Kind
Full House
Three-of-a-Kind
Two Pair
One Pair
High Card only
Once the ranking of the hands has been given you need to get a sum of all the Hands bid multiplied by their ranking.
Part 2:
Part 2 added complexity in that you could now use the 'J' card as a Joker (wildcard) meaning it could be used to increase your hand, e.g if you had KKKQJ
rather than Three-of-a-kind on Kings, could up to Four-of-a-kind Kings with the Joker card.
Solution:
class Program
{
static void Main()
{
string input = File.ReadAllText("./puzzle.txt");
string[] lines = input.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
Console.WriteLine(Part1(lines));
Console.WriteLine(Part2(lines));
}
static long Part1(string[] lines)
{
var hands = lines.Select(Hand.Parse).ToArray();
return TotalWinnings(hands);
}
static long Part2(string[] lines)
{
Hand[] hands = [.. lines.Select(s => s.Replace('J', '*')).Select(Hand.Parse)];
return TotalWinnings(hands);
}
public static long TotalWinnings(Hand[] hands)
{
var sorted = hands.OrderBy(hand => hand.WinningHandRank)
.ThenBy(hand => hand.Cards, new CardArrayComparer());
return sorted.Select((hand, index) => hand.Bid * (index + 1)).Sum();
}
#region Records
public record Hand(Card[] Cards, long Bid)
{
public WinningHandRank WinningHandRank => GetWinningHandRank(Cards);
public static Hand Parse(string input)
{
string[] parts = input.Split(new char[0], StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
Card[] cards = [.. parts[0].Select(Card.Parse)];
long bid = long.Parse(parts[1]);
return new Hand(cards, bid);
}
public static WinningHandRank GetWinningHandRank(Card[] cards)
{
var nonWildCards = cards.Where(c => c.Rank > 1); // gets all Non-Joker cards
int count = nonWildCards.Count();
// no substitutions needed
if (count == 5) { return GetTypeOfHand(cards); }
int wildCards = 5 - count;
IEnumerable<Card[]> substitutions = Card.All.Select(c => Enumerable.Repeat(c, wildCards).ToArray());
WinningHandRank best = WinningHandRank.HighCard;
foreach (Card[] substitution in substitutions)
{
Card[] p = [.. nonWildCards, .. substitution];
WinningHandRank current = GetTypeOfHand(p);
best = (WinningHandRank)Math.Max((int)best, (int)current);
}
return best;
}
private static WinningHandRank GetTypeOfHand(Card[] cards)
{
int[] counts = [..
cards
.GroupBy(c => c)
.Select(g => g.Count())
.OrderDescending()
];
return counts switch
{
[5] => WinningHandRank.FiveOfAKind,
[4, 1] => WinningHandRank.FourOfAKind,
[3, 2] => WinningHandRank.FullHouse,
[3, 1, 1] => WinningHandRank.ThreeOfAKind,
[2, 2, 1] => WinningHandRank.TwoPair,
[2, 1, 1, 1] => WinningHandRank.OnePair,
_ => WinningHandRank.HighCard,
};
}
}
public record Card(long Rank, char mappedCharacter)
{
public static Card[] All { get; } =
[
new Card(2, '2'),
new Card(3, '3'),
new Card(4, '4'),
new Card(5, '5'),
new Card(6, '6'),
new Card(7, '7'),
new Card(8, '8'),
new Card(9, '9'),
new Card(10, 'T'),
new Card(12, 'Q'),
new Card(13, 'K'),
new Card(14, 'A'),
];
public static Card Parse(char ch)
{
int rank = ch switch
{
'*' => 1,
'2' => 2,
'3' => 3,
'4' => 4,
'5' => 5,
'6' => 6,
'7' => 7,
'8' => 8,
'9' => 9,
'T' => 10,
'J' => 11,
'Q' => 12,
'K' => 13,
'A' => 14,
_ => throw new InvalidOperationException($"Invalid card rank: '{ch}'")
};
return new Card(rank, ch);
}
}
public enum WinningHandRank
{
HighCard = 0,
OnePair = 1,
TwoPair = 2,
ThreeOfAKind = 3,
FullHouse = 4,
FourOfAKind = 5,
FiveOfAKind = 6,
}
public class CardArrayComparer : IComparer<Card[]>
{
public int Compare(Card[] x, Card[] y)
{
for (int i = 0; i < x.Length; i++)
{
int compareResult = x[i].Rank.CompareTo(y[i].Rank);
if (compareResult != 0)
{
return compareResult;
}
}
return 0; // Arrays are equal
}
}
}
#endregion
BreakDown
var hands = lines.Select(Hand.Parse).ToArray();
The first thing to do was Parse each line to a Hand object. Giving each card a weighting, meaning 2 got a value of 2, through to Ace Card which got a value of 14.
The Card.Parse function takes the character, maps it to a weighting value, and then returns a Card
object with a rank and the original character.
TotalWinning
Once we have our hands it's the big task of working out the winnings, via the TotalWinnings
method.
This is done in two stages,
var sorted = hands.OrderBy(hand => hand.WinningHandRank)
.ThenBy(hand => hand.Cards, new CardArrayComparer());
First Stage - Ordering By Winning Hand Rank
Order the hands, by their WinningHandRank, which is a computed property on the Hand object.
public WinningHandRank WinningHandRank => GetWinningHandRank(Cards);
This says when the WinningHandRank is called, return the GetWinningHandRank(Cards)
function with the current Hand's cards.
GetWinningHandRank Breakdown
This is where all the work happens.
For Part 1 -> it was a lot simpler we just didn't have to concern ourselves with Joker cards, and substitutions, and could just run the GetTypeOfHand
function.
However for the full solution
Step 1:
var nonWildCards = cards.Where(c => c.Rank > 1);
int count = nonWildCards.Count();
Get all the cards that have a higher rank of 1 (remember we gave *
a rank of 1 earlier (our wildcard initial value). Then count how many of these we have.
Step 2:
// no substitutions needed
if (count == 5) { return GetTypeOfHand(cards); }
If the count is 5 we know that there are no jokers. So, therefore, we don't need any substitutions and just find the Type of Hand which we have.
Step 3:
If the count is < 5 we know that there must be at least one Joker card, and need to apply a substitution.
But wait how do we know what is the best card to change it to?
Well, at this moment we don't, which is where Step 4 comes in.
Step 4:
IEnumerable<Card[]> substitutions = Card.All.Select(c => Enumerable.Repeat(c, wildCards).ToArray());
Card.All
is a property on the Card
record, which returns ALL of the cards we could change it to with their Rank and corresponding character (which we will need later to help us switch *
-> character.
You should now be familiar with the Select
function, but for a recap, it simply applies the function passed to it to each element in the collection.
In this case, we're saying return Enumerable.Repeat(c, wildCards)
for each element in Card.All.
Wait a new method, what is that doing ?
Repeat
takes a constant value which will be returned each time, as well as a number of how many times to repeat.
So here, we're passing in a constant, which is the Card (c
) variable, and wildcards
(the count of how many wildcards we want to swap.
Example:
Starting Hand => KKJJ2 (King King Joker Joker 2).
After Parsing => KK**2
Wildcards = 2
1st iteration: C is 2, Wildcards is 2, returns [2,2] two 2 cards.
2nd Iteration: C is 3, Wildcards is 2, returns [3,3]
and so on... up to C is A, Wildcards is 2 returning [A, A].
This means at the end of the code substitutions
now has a List of arrays of possible cards and their quantity.
So we'd have a list of [2,2],[3,3],[4,4] -> [A,A];
Step 5:
Now we have our substitutions (the possible cards the wildcards could be) bearing in mind we want them to be the same, as that's worth more, e.g it would be pointless doing 1 x 2 and 1 x K, when 2 x K is worth more, and we want the highest ranking hand we can get.
We combine the substitutions, with the non wild cards to make a hand.
Step 6:
Loop over all the possible substitutions and run each through our GetHandType
method to get our hand type value.
Then compare the current HandType with the best
. If our current hand is better than the best one, we re-assign the best hand to our current. We keep going until all the substitutions have been handled.
By the end of it, we have the highest hand we can make the substitutions, and this is returned.
So taking the example earlier, we'd cycle through all the substitutions, 22KKQ, 33KKQ, 44KKQ -> hit KKKKQ this is now the best, as the final substitution of [A, A] AAKKQ isn't as good (Two Pair vs 4 of a kind).
Back to TotalWinnings
So using the enum value returned from GetWinningHandRank within the OrderBy method, the items can be sorted.
Once we've sorted them by HandType, we then order them again by our customer Comparer
.
Comparer Class
The CardArrayComparer
Class inherits from IComparer<Card[]>
, and as a result must implement the Compare
method. To learn more about inheritance and interfaces check out my tutorial here.
The Compare
method,
for (int i = 0; i < x.Length; i++)
{
int compareResult = x[i].Rank.CompareTo(y[i].Rank);
if (compareResult != 0)
{
return compareResult;
}
}
return 0; // Arrays are equal
}
The method iterates through each index of the arrays (x and y) in a loop.
At each index i, it compares the ranks of the i-th cards in the arrays using the CompareTo method.
If the ranks are not equal (compareResult != 0), the method returns the result of the comparison immediately, indicating the relative order of the two arrays.
If the ranks are equal for all cards in the arrays, the method proceeds to the next index.
Final Step
return sorted.Select((hand, index) => hand.Bid * (index + 1)).Sum();
Now the hands are sorted we can utilise Linq to Select
the Hand.Bid
multiplied by the index + 1, we add 1 as the index begins at 0 and we want the minimum rank is 1.
Once all rankings and bids have been calculated, we can Sum()
the winnings.
I hope this rather extensive tutorial is helpful and helps you solve the challenge, and maybe teach you something new about C#.
As always, don't forget to follow for future posts, or reach out on Twitter.
Top comments (0)