From a84b0d07f063fc2e013c3c71af196d40a526c82b Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Thu, 7 Dec 2023 14:06:26 -0600 Subject: [PATCH] feat: solve part 1 --- 07/CamelCards.Tests/CamelCards.Tests.csproj | 36 ++++ 07/CamelCards.Tests/CardTests.cs | 31 +++ 07/CamelCards.Tests/GlobalUsings.cs | 2 + 07/CamelCards.Tests/HandTests.cs | 204 ++++++++++++++++++++ 07/CamelCards.Tests/ProgramTests.cs | 11 ++ 07/CamelCards.Tests/TurnTests.cs | 95 +++++++++ 07/CamelCards/CamelCards.csproj | 10 + 07/CamelCards/Program.cs | 173 +++++++++++++++++ AdventOfCode2023.sln | 16 ++ 9 files changed, 578 insertions(+) create mode 100644 07/CamelCards.Tests/CamelCards.Tests.csproj create mode 100644 07/CamelCards.Tests/CardTests.cs create mode 100644 07/CamelCards.Tests/GlobalUsings.cs create mode 100644 07/CamelCards.Tests/HandTests.cs create mode 100644 07/CamelCards.Tests/ProgramTests.cs create mode 100644 07/CamelCards.Tests/TurnTests.cs create mode 100644 07/CamelCards/CamelCards.csproj create mode 100644 07/CamelCards/Program.cs diff --git a/07/CamelCards.Tests/CamelCards.Tests.csproj b/07/CamelCards.Tests/CamelCards.Tests.csproj new file mode 100644 index 0000000..9005157 --- /dev/null +++ b/07/CamelCards.Tests/CamelCards.Tests.csproj @@ -0,0 +1,36 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + PreserveNewest + + + + diff --git a/07/CamelCards.Tests/CardTests.cs b/07/CamelCards.Tests/CardTests.cs new file mode 100644 index 0000000..28bb3d0 --- /dev/null +++ b/07/CamelCards.Tests/CardTests.cs @@ -0,0 +1,31 @@ +namespace CamelCards.Tests; + +public class CardTests +{ + [Theory] + [InlineData('A', 12)] + [InlineData('K', 11)] + [InlineData('Q', 10)] + [InlineData('J', 9)] + [InlineData('T', 8)] + [InlineData('9', 7)] + [InlineData('8', 6)] + [InlineData('7', 5)] + [InlineData('6', 4)] + [InlineData('5', 3)] + [InlineData('4', 2)] + [InlineData('3', 1)] + [InlineData('2', 0)] + public void Card_WhenGivenValidCharacter_ItShouldReturnCardWithExpectedStrength(char character, int expectedStrength) + { + var card = new Card(character); + card.Strength.Should().Be(expectedStrength); + } + + [Fact] + public void Card_WhenGivenInvalidCardCharacter_ItShouldThrowArgumentException() + { + Action act = () => new Card('X'); + act.Should().Throw(); + } +} \ No newline at end of file diff --git a/07/CamelCards.Tests/GlobalUsings.cs b/07/CamelCards.Tests/GlobalUsings.cs new file mode 100644 index 0000000..7fef4b0 --- /dev/null +++ b/07/CamelCards.Tests/GlobalUsings.cs @@ -0,0 +1,2 @@ +global using Xunit; +global using FluentAssertions; \ No newline at end of file diff --git a/07/CamelCards.Tests/HandTests.cs b/07/CamelCards.Tests/HandTests.cs new file mode 100644 index 0000000..6f75abc --- /dev/null +++ b/07/CamelCards.Tests/HandTests.cs @@ -0,0 +1,204 @@ +namespace CamelCards.Tests; + +public class HandTests +{ + [Theory] + [MemberData(nameof(TestData.HandTypeTestData), MemberType = typeof(TestData))] + public void Type_WhenGivenListOfCards_ItShouldReturnExpectedHandType(List cards, HandType expectedHandType) + { + var hand = new Hand(cards); + hand.Type.Should().Be(expectedHandType); + } + + [Theory] + [MemberData(nameof(TestData.CompareToTestData), MemberType = typeof(TestData))] + public void CompareTo_WhenGivenHandWithHigherType_ItShouldReturnNegative1(Hand hand, Hand otherHand, int sortResult) + { + hand.CompareTo(otherHand).Should().Be(sortResult); + } + + public static class TestData + { + public static IEnumerable CompareToTestData => + new List + { + new object[] + { + new Hand( + [ + new('A'), + new('A'), + new('A'), + new('A'), + new('A'), + ] + ), + new Hand( + [ + new('A'), + new('A'), + new('8'), + new('A'), + new('A'), + ] + ), + 1, + }, + new object[] + { + new Hand( + [ + new('A'), + new('A'), + new('8'), + new('A'), + new('A'), + ] + ), + new Hand( + [ + new('A'), + new('A'), + new('A'), + new('A'), + new('A'), + ] + ), + -1, + }, + new object[] + { + new Hand( + [ + new('A'), + new('A'), + new('A'), + new('A'), + new('A'), + ] + ), + new Hand( + [ + new('A'), + new('A'), + new('A'), + new('A'), + new('A'), + ] + ), + 0, + }, + new object[] + { + new Hand( + [ + new('3'), + new('3'), + new('3'), + new('3'), + new('2'), + ] + ), + new Hand( + [ + new('2'), + new('A'), + new('A'), + new('A'), + new('A'), + ] + ), + 1, + } + }; + + public static IEnumerable HandTypeTestData => + new List + { + new object[] + { + new List + { + new('A'), + new('A'), + new('A'), + new('A'), + new('A'), + }, + HandType.FiveOfAKind, + }, + new object[] + { + new List + { + new('A'), + new('A'), + new('8'), + new('A'), + new('A'), + }, + HandType.FourOfAKind, + }, + new object[] + { + new List + { + new('2'), + new('3'), + new('3'), + new('3'), + new('2'), + }, + HandType.FullHouse, + }, + new object[] + { + new List + { + new('T'), + new('T'), + new('T'), + new('9'), + new('8'), + }, + HandType.ThreeOfAKind, + }, + new object[] + { + new List + { + new('2'), + new('3'), + new('4'), + new('3'), + new('2'), + }, + HandType.TwoPair, + }, + new object[] + { + new List + { + new('A'), + new('2'), + new('3'), + new('A'), + new('4'), + }, + HandType.OnePair, + }, + new object[] + { + new List + { + new('2'), + new('3'), + new('4'), + new('5'), + new('6'), + }, + HandType.HighCard, + }, + }; + } +} \ No newline at end of file diff --git a/07/CamelCards.Tests/ProgramTests.cs b/07/CamelCards.Tests/ProgramTests.cs new file mode 100644 index 0000000..6e0791a --- /dev/null +++ b/07/CamelCards.Tests/ProgramTests.cs @@ -0,0 +1,11 @@ +namespace CamelCards.Tests; + +public class ProgramTests +{ + [Fact] + public async Task Main_WhenCalledWithInputFile_ItShouldReturnExpectedResult() + { + var result = await Program.Main(["INPUT.txt"]); + result.Should().Be(251287184); + } +} \ No newline at end of file diff --git a/07/CamelCards.Tests/TurnTests.cs b/07/CamelCards.Tests/TurnTests.cs new file mode 100644 index 0000000..58e2f51 --- /dev/null +++ b/07/CamelCards.Tests/TurnTests.cs @@ -0,0 +1,95 @@ +namespace CamelCards.Tests; + +public class TurnTests +{ + [Fact] + public void Parse_WhenGivenTurnInput_ItShouldReturnExpectedTurnInstance() + { + var turn = Turn.Parse("32T3K 765"); + turn.Hand.Cards.Should().BeEquivalentTo(new List + { + new('3'), + new('2'), + new('T'), + new('3'), + new('K'), + }); + + turn.Bid.Should().Be(765); + } + + [Fact] + public void ToString_WhenCalled_ItShouldReturnStringRepresentationOfTurn() + { + var turn = "32T3K 765"; + Turn.Parse(turn).ToString().Should().Be(turn); + } + + [Fact] + public void OrderBy_WhenGivenListOfTurns_ItShouldBeAbleToOrderThemAccordingToHandStrength() + { + var turns = TestData.TestTurns + .Select(Turn.Parse) + .ToList(); + + turns + .OrderBy(t => t.Hand) + .Select(t => t.ToString()) + .Should() + .BeEquivalentTo( + [ + "32T3K 765", + "KTJJT 220", + "KK677 28", + "T55J5 684", + "QQQJA 483", + ] + ); + } + + [Fact] + public void OrderByDescending_WhenGivenListOfTurns_ItShouldBeAbleToOrderThemAccordingToHandStrength() + { + var turns = TestData.TestTurns + .Select(Turn.Parse) + .ToList(); + + turns + .OrderByDescending(t => t.Hand) + .Select(t => t.ToString()) + .Should() + .BeEquivalentTo( + [ + "QQQJA 483", + "T55J5 684", + "KK677 28", + "KTJJT 220", + "32T3K 765", + ] + ); + } + + [Fact] + public void Turn_GivenListOfTurns_ItShouldBeAbleToCalculateTotalWinnings() + { + TestData.TestTurns + .Select(Turn.Parse) + .OrderBy(t => t.Hand) + .Select((turn, index) => turn.Bid * (index + 1)) + .Sum() + .Should() + .Be(6440); + } + + public static class TestData + { + public static readonly string[] TestTurns = + [ + "32T3K 765", + "T55J5 684", + "KK677 28", + "KTJJT 220", + "QQQJA 483" + ]; + } +} \ No newline at end of file diff --git a/07/CamelCards/CamelCards.csproj b/07/CamelCards/CamelCards.csproj new file mode 100644 index 0000000..2150e37 --- /dev/null +++ b/07/CamelCards/CamelCards.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + enable + enable + + + diff --git a/07/CamelCards/Program.cs b/07/CamelCards/Program.cs new file mode 100644 index 0000000..30ce0e2 --- /dev/null +++ b/07/CamelCards/Program.cs @@ -0,0 +1,173 @@ +using System.Diagnostics; + +namespace CamelCards; + + +public class Program +{ + public static async Task Main(string[] args) + { + if (args.Length is 0) + { + Console.WriteLine("Please provide a path to the input file."); + return -1; + } + + if (File.Exists(args[0]) is false) + { + Console.WriteLine("The provided file does not exist."); + return -2; + } + + var input = await File.ReadAllLinesAsync(args[0]); + + var stopWatch = new Stopwatch(); + stopWatch.Start(); + + var result = input + .Select(Turn.Parse) + .OrderBy(t => t.Hand) + .Select((turn, index) => turn.Bid * (index + 1)) + .Sum(); + + stopWatch.Stop(); + + Console.WriteLine($"The total winnings is {result}. ({stopWatch.ElapsedMilliseconds}ms)"); + + return result; + } +} + +public class Turn( + Hand hand, + int bid +) +{ + public Hand Hand { get; init; } = hand; + public int Bid { get; init; } = bid; + + public override string ToString() + { + return $"{string.Join("", Hand.Cards.Select(c => c.Value))} {Bid}"; + } + + public static Turn Parse(string turnInput) + { + var parts = turnInput.Split(' ', StringSplitOptions.TrimEntries); + var cards = parts[0].Select(c => new Card(c)).ToList(); + var bid = int.Parse(parts[1]); + + return new Turn(new(cards), bid); + } +} + +public class Hand : IComparable +{ + public List Cards { get; init; } + + public Hand(List cards) + { + if (cards.Count != 5) + { + throw new ArgumentException("A hand must have 5 cards"); + } + + Cards = cards; + } + + public HandType Type => Cards.GroupBy(c => c.Value).Count() switch + { + 5 => HandType.HighCard, + 4 => HandType.OnePair, + 3 => Cards.GroupBy(c => c.Value).Any(g => g.Count() == 3) ? HandType.ThreeOfAKind : HandType.TwoPair, + 2 => Cards.GroupBy(c => c.Value).Any(g => g.Count() == 4) ? HandType.FourOfAKind : HandType.FullHouse, + 1 => HandType.FiveOfAKind, + _ => throw new ApplicationException("Hand has no defined type"), + }; + + public int CompareTo(Hand? other) + { + int result; + + if (other == null) + { + return 1; + } + + result = Type.CompareTo(other.Type); + + if (result != 0) + { + return result; + } + + for (int i = 0; i < Cards.Count; i++) + { + var currentCard = Cards[i]; + var otherCard = other.Cards[i]; + + var cardComparison = currentCard.Strength.CompareTo(otherCard.Strength); + + if (i == Cards.Count - 1) + { + result = cardComparison; + } + + if (cardComparison != 0) + { + result = cardComparison; + break; + } + } + + return result; + } +} + +public enum HandType +{ + HighCard, + OnePair, + TwoPair, + ThreeOfAKind, + FullHouse, + FourOfAKind, + FiveOfAKind, +} + + +public class Card +{ + private static readonly Dictionary Cards = new() + { + ['A'] = 12, + ['K'] = 11, + ['Q'] = 10, + ['J'] = 9, + ['T'] = 8, + ['9'] = 7, + ['8'] = 6, + ['7'] = 5, + ['6'] = 4, + ['5'] = 3, + ['4'] = 2, + ['3'] = 1, + ['2'] = 0, + }; + + public char Value { get; init; } + public int Strength { get; init; } + + public Card(char value) + { + var isValidCardChar = Cards.TryGetValue(value, out var strength); + + if (!isValidCardChar) + { + throw new ArgumentException($"Invalid card value: {value}"); + } + + Value = value; + Strength = strength; + } +} \ No newline at end of file diff --git a/AdventOfCode2023.sln b/AdventOfCode2023.sln index 8590e39..e11e266 100644 --- a/AdventOfCode2023.sln +++ b/AdventOfCode2023.sln @@ -39,6 +39,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WaitForIt", "06\WaitForIt\W EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WaitForIt.Tests", "06\WaitForIt.Tests\WaitForIt.Tests.csproj", "{8C89E1D0-2617-4B89-BA9C-9189FC69BC43}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "07", "07", "{7F8AD027-D8D7-407B-9E08-B363B7B34621}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CamelCards", "07\CamelCards\CamelCards.csproj", "{DC16CBE1-B1FA-4E6B-AAD8-43CDECA46659}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CamelCards.Tests", "07\CamelCards.Tests\CamelCards.Tests.csproj", "{8F04A78E-D2E1-41AC-B43C-74608B7B4FCB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -96,6 +102,14 @@ Global {8C89E1D0-2617-4B89-BA9C-9189FC69BC43}.Debug|Any CPU.Build.0 = Debug|Any CPU {8C89E1D0-2617-4B89-BA9C-9189FC69BC43}.Release|Any CPU.ActiveCfg = Release|Any CPU {8C89E1D0-2617-4B89-BA9C-9189FC69BC43}.Release|Any CPU.Build.0 = Release|Any CPU + {DC16CBE1-B1FA-4E6B-AAD8-43CDECA46659}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DC16CBE1-B1FA-4E6B-AAD8-43CDECA46659}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DC16CBE1-B1FA-4E6B-AAD8-43CDECA46659}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DC16CBE1-B1FA-4E6B-AAD8-43CDECA46659}.Release|Any CPU.Build.0 = Release|Any CPU + {8F04A78E-D2E1-41AC-B43C-74608B7B4FCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8F04A78E-D2E1-41AC-B43C-74608B7B4FCB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8F04A78E-D2E1-41AC-B43C-74608B7B4FCB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8F04A78E-D2E1-41AC-B43C-74608B7B4FCB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {3F8EAC09-4BC7-43AA-B72B-48DDD2710F6D} = {8C29858C-623A-461A-BF0B-254E151CD9C2} @@ -110,5 +124,7 @@ Global {CB90004D-9420-4411-9110-B50274537FE5} = {5067FD6B-8F03-4502-AB83-8A391D801B47} {B3D736D4-EFEE-44AA-B199-1A1C803F1793} = {86704890-D6B8-4166-853B-F1424B8396C7} {8C89E1D0-2617-4B89-BA9C-9189FC69BC43} = {86704890-D6B8-4166-853B-F1424B8396C7} + {DC16CBE1-B1FA-4E6B-AAD8-43CDECA46659} = {7F8AD027-D8D7-407B-9E08-B363B7B34621} + {8F04A78E-D2E1-41AC-B43C-74608B7B4FCB} = {7F8AD027-D8D7-407B-9E08-B363B7B34621} EndGlobalSection EndGlobal