namespace AOC.Tests.Y2023; [TestFixture] [Parallelizable(ParallelScope.All)] public class Day07 { [SetUp] public void Setup() { realData = File.ReadAllLines(Path.Combine(TestContext.CurrentContext.TestDirectory, "Y2023", "Data", $"{GetThisClassName()}.dat")); } protected string GetThisClassName() { return GetType().Name; } private string[] realData; [TestCase(@"32T3K 765 T55J5 684 KK677 28 KTJJT 220 QQQJA 483", 6440)] [TestCase(null, 247815719)] // The actual answer public void Part1(string input, int? expected) { string[] lines = input != null ? input.Split("\n") : realData; List hands = new(); foreach (string line in lines) { string[] parts = line.Split(" "); string cards = parts[0]; int bid = int.Parse(parts[1]); Hand hand = new(cards, bid); hands.Add(hand); } ParallelMergeSort.Sort(hands); int totalWinnings = 0; int rank = 1; foreach (Hand hand in hands) { int handWinnings = hand.Bid * rank; totalWinnings += handWinnings; // Console.WriteLine($"Hand: {hand.Cards}, Rank: {rank}, Winnings: {handWinnings}"); rank++; } int result = totalWinnings; if (expected != null) { Assert.That(result, Is.EqualTo(expected.Value)); } Console.WriteLine($"Part 1: {result}"); } [TestCase(@"32T3K 765 T55J5 684 KK677 28 KTJJT 220 QQQJA 483", 5905)] [TestCase(@"2345A 1 Q2KJJ 13 Q2Q2Q 19 T3T3J 17 T3Q33 11 2345J 3 J345A 2 32T3K 5 T55J5 29 KK677 7 KTJJT 34 QQQJA 31 JJJJJ 37 JAAAA 43 AAAAJ 59 AAAAA 61 2AAAA 23 2JJJJ 53 JJJJ2 41", 6839)] [TestCase(null, 248747492)] // The actual answer public void Part2(string input, int? expected) { string[] lines = input != null ? input.Split("\n") : realData; List hands = new(); foreach (string line in lines) { string[] parts = line.Split(" "); string cards = parts[0]; int bid = int.Parse(parts[1]); Hand hand = new(cards, bid, true); hands.Add(hand); } ParallelMergeSort.Sort(hands); int totalWinnings = 0; int rank = 1; foreach (Hand hand in hands) { int handWinnings = hand.Bid * rank; totalWinnings += handWinnings; Console.WriteLine( $"Hand: {hand.Cards}, Type: {hand.Type.ToString()}, Rank: {rank}, Winnings: {handWinnings}"); rank++; } int result = totalWinnings; if (expected != null) { Assert.That(result, Is.EqualTo(expected.Value)); } Console.WriteLine($"Part 2: {result}"); } } public enum HandType { HighCard, OnePair, TwoPair, ThreeOfAKind, FullHouse, FourOfAKind, FiveOfAKind } public class Hand : IComparable { private readonly bool _useJokerLogic; public Hand(string cards, int bid, bool useJokerLogic = false) { _useJokerLogic = useJokerLogic; Cards = cards; Bid = bid; DetermineHandType(); } public string Cards { get; } public int Bid { get; } public HandType Type { get; set; } public int CompareTo(Hand other) { // First compare based on hand type if (Type != other.Type) { return Type.CompareTo(other.Type); } // If types are the same, then compare based on individual card strength // Convert card labels to a list of integers for easier comparison List thisCardStrengths = ConvertToCardStrengths(Cards, _useJokerLogic); List otherCardStrengths = ConvertToCardStrengths(other.Cards, _useJokerLogic); for (int i = 0; i < thisCardStrengths.Count; i++) { if (thisCardStrengths[i] != otherCardStrengths[i]) { return thisCardStrengths[i].CompareTo(otherCardStrengths[i]); } } // If all cards are the same, the hands are equal return 0; } private void DetermineHandType() { // Count occurrences of each card Dictionary cardCounts = new(); int jokers = 0; foreach (char card in Cards) { if (_useJokerLogic && card == 'J') { jokers++; continue; } if (cardCounts.ContainsKey(card)) { cardCounts[card]++; } else { cardCounts.Add(card, 1); } } if (_useJokerLogic && jokers > 0) { ApplyJokerLogic(cardCounts, jokers); } else { DetermineStandardHandType(cardCounts); } } private void ApplyJokerLogic(Dictionary cardCounts, int jokers) { // Get the maximum amount of any single card in the hand int maxCount = cardCounts.Any() ? cardCounts.Max(c => c.Value) : 0; // FiveOfAKind if (maxCount == 5 || maxCount + jokers == 5) { Type = HandType.FiveOfAKind; return; } // FourOfAKind if (maxCount == 4 || maxCount + jokers == 4) { Type = HandType.FourOfAKind; return; } // FullHouse // if there are 3 of one card and 2 of another // or if there are three of one card and one joker // or if there are two of one card and two jokers if ( (cardCounts.Any(c => c.Value == 3) && cardCounts.Any(c => c.Value == 2)) || (cardCounts.Any(c => c.Value == 3) && jokers == 1) || (cardCounts.Any(c => c.Value == 2) && jokers == 2) || (cardCounts.Count(c => c.Value == 2) == 2 && jokers == 1) ) { Type = HandType.FullHouse; return; } // ThreeOfAKind // if there are three of one card // or if there are two of one card and one joker // or if there are two jokers if (cardCounts.Any(c => c.Value == 3) || (cardCounts.Any(c => c.Value == 2) && jokers == 1) || jokers == 2) { Type = HandType.ThreeOfAKind; return; } // TwoPair // if there are two of two cards // or if there is two of one card and two jokers // or if there are three jokers if (cardCounts.Count(c => c.Value == 2) == 2 || (cardCounts.Any(c => c.Value == 2) && jokers == 1) || jokers == 3) { Type = HandType.TwoPair; return; } // OnePair // if there is two of one card // or if there is one joker if (cardCounts.Count(c => c.Value == 2) == 2 || jokers == 1) { Type = HandType.OnePair; return; } // HighCard // If no other hand can be formed, use a high card Type = HandType.HighCard; } private void DetermineStandardHandType(Dictionary cardCounts) { // Sort counts to help identify patterns like full house or two pairs List sortedCounts = cardCounts.Values.OrderByDescending(count => count).ToList(); Type = sortedCounts[0] switch { // Determine hand type based on the sorted counts 5 => HandType.FiveOfAKind, 4 => HandType.FourOfAKind, 3 when sortedCounts[1] == 2 => HandType.FullHouse, 3 => HandType.ThreeOfAKind, 2 when sortedCounts[1] == 2 => HandType.TwoPair, 2 => HandType.OnePair, _ => HandType.HighCard }; } private static List ConvertToCardStrengths(string cards, bool useJokerLogic) { Dictionary cardStrength = new() { { 'A', 14 }, { 'K', 13 }, { 'Q', 12 }, { 'T', 10 }, { '9', 9 }, { '8', 8 }, { '7', 7 }, { '6', 6 }, { '5', 5 }, { '4', 4 }, { '3', 3 }, { '2', 2 }, { 'J', useJokerLogic ? 1 : 11 } // 'J' is weakest if joker logic is enabled }; return cards.Select(c => cardStrength[c]).ToList(); } } public static class ParallelMergeSort { private const int Threshold = 100; public static void Sort(List hands) { if (hands.Count <= 1) { return; } int mid = hands.Count / 2; List left = new(hands.GetRange(0, mid)); List right = new(hands.GetRange(mid, hands.Count - mid)); if (hands.Count <= Threshold) { // Sequential sort for small lists Sort(left); Sort(right); } else { // Sort in parallel for large lists Task[] tasks = new Task[2]; tasks[0] = Task.Factory.StartNew(() => Sort(left)); tasks[1] = Task.Factory.StartNew(() => Sort(right)); Task.WaitAll(tasks); } Merge(hands, left, right); } private static void Merge(List hands, List left, List right) { int i = 0, j = 0, k = 0; while (i < left.Count && j < right.Count) { if (left[i].CompareTo(right[j]) <= 0) { hands[k++] = left[i++]; } else { hands[k++] = right[j++]; } } while (i < left.Count) { hands[k++] = left[i++]; } while (j < right.Count) { hands[k++] = right[j++]; } } }