Advent of Code: Day 7 - Camel Cards
Tags: adventofcode, csharp, tutorial
Published: 2023-12-09 18:34:34

## Day 7: Camel Cards

[link to challenge](https://adventofcode.com/2023/day/7)

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.

[link to Github](https://github.com/gweaths/advent_code_2023)

[Brief](#tldr-of-problem)
[Solution](#solution)
[Breakdown](#breakdown)
[Comparer Class](#comparer-class)
[Final Step](#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:

```csharp
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

```csharp
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, 

```csharp 
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. 

```csharp
 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:**
``` csharp
 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:**
```csharp
// 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:**

```csharp 
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](https://dev.to/gweaths/c-inheritance-pt2-interfaces-339c).

The `Compare` method, 

```csharp
 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

```csharp
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](http://twitter.com/gweaths).