Created
December 11, 2023 01:06
-
-
Save CJSmith-0141/e05da724e4d4ae41c689122fc51cc083 to your computer and use it in GitHub Desktop.
Aoc 2023 Day 7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package net.tazato | |
| import cats.Eq | |
| import cats.effect.* | |
| import cats.syntax.all.* | |
| import cats.parse.Parser as P | |
| import cats.parse.Numbers.digits | |
| import cats.parse.Rfc5234.{alpha, lf, wsp, octet} | |
| object Day7 extends IOApp.Simple { | |
| val input = io.Source.fromResource("day7.txt").mkString | |
| enum Card(val value: Int): | |
| case Two extends Card(2) | |
| case Three extends Card(3) | |
| case Four extends Card(4) | |
| case Five extends Card(5) | |
| case Six extends Card(6) | |
| case Seven extends Card(7) | |
| case Eight extends Card(8) | |
| case Nine extends Card(9) | |
| case Ten extends Card(10) | |
| case Jack extends Card(11) | |
| case Queen extends Card(12) | |
| case King extends Card(13) | |
| case Ace extends Card(14) | |
| case Joker extends Card(-1) | |
| object Card { | |
| given Eq[Card] with | |
| def eqv(c1: Card, c2: Card): Boolean = c1.value == c2.value | |
| given Ordering[Card] with | |
| def compare(c1: Card, c2: Card): Int = c1.value - c2.value | |
| extension (v: Int) | |
| def toCard: Option[Card] = v match { | |
| case 2 => Some(Card.Two) | |
| case 3 => Some(Card.Three) | |
| case 4 => Some(Card.Four) | |
| case 5 => Some(Card.Five) | |
| case 6 => Some(Card.Six) | |
| case 7 => Some(Card.Seven) | |
| case 8 => Some(Card.Eight) | |
| case 9 => Some(Card.Nine) | |
| case 10 => Some(Card.Ten) | |
| case 11 => Some(Card.Jack) | |
| case 12 => Some(Card.Queen) | |
| case 13 => Some(Card.King) | |
| case 14 => Some(Card.Ace) | |
| case -1 => Some(Card.Joker) | |
| case _ => None | |
| } | |
| extension (c: Char) | |
| def toCard(withJokers: Boolean): Option[Card] = c match { | |
| case '2' => Some(Card.Two) | |
| case '3' => Some(Card.Three) | |
| case '4' => Some(Card.Four) | |
| case '5' => Some(Card.Five) | |
| case '6' => Some(Card.Six) | |
| case '7' => Some(Card.Seven) | |
| case '8' => Some(Card.Eight) | |
| case '9' => Some(Card.Nine) | |
| case 'T' => Some(Card.Ten) | |
| case 'J' => if (withJokers) Some(Card.Joker) else Some(Card.Jack) | |
| case 'Q' => Some(Card.Queen) | |
| case 'K' => Some(Card.King) | |
| case 'A' => Some(Card.Ace) | |
| case _ => None | |
| } | |
| } | |
| enum Hand(val cards: List[Card]): | |
| case HighCard(v: List[Card]) extends Hand(v) | |
| case OnePair(v: List[Card]) extends Hand(v) | |
| case TwoPair(v: List[Card]) extends Hand(v) | |
| case ThreeOfAKind(v: List[Card]) extends Hand(v) | |
| case FullHouse(v: List[Card]) extends Hand(v) | |
| case FourOfAKind(v: List[Card]) extends Hand(v) | |
| case FiveOfAKind(v: List[Card]) extends Hand(v) | |
| object Hand { | |
| def fromCards(cards: List[Card]): Hand = { | |
| val grouped = cards.groupBy(_.value).view.mapValues(_.size).toMap | |
| grouped match { | |
| case m if m.values.exists(_ == 5) => FiveOfAKind(cards) | |
| case m if m.values.exists(_ == 4) => FourOfAKind(cards) | |
| case m if m.values.exists(_ == 3) && m.values.exists(_ == 2) => | |
| FullHouse(cards) | |
| case m if m.values.exists(_ == 3) => ThreeOfAKind(cards) | |
| case m if m.values.exists(_ == 2) && m.values.size == 3 => | |
| TwoPair(cards) | |
| case m if m.values.exists(_ == 2) => OnePair(cards) | |
| case _ => HighCard(cards) | |
| } | |
| } | |
| given order: Ordering[Hand] with | |
| def compare(h1: Hand, h2: Hand): Int = | |
| if (h1.ordinal != h2.ordinal) h1.ordinal - h2.ordinal | |
| else { | |
| h1.cards | |
| .zip(h2.cards) | |
| .filterNot((c1, c2) => c1.value == c2.value) | |
| .headOption match { | |
| case Some((c1, c2)) => c1.value - c2.value | |
| case None => 0 | |
| } | |
| } | |
| given Eq[Hand] with | |
| def eqv(h1: Hand, h2: Hand): Boolean = | |
| h1.ordinal == h2.ordinal && h1.cards == h2.cards | |
| extension (l: List[Card]) def toHand: Hand = Hand.fromCards(l) | |
| } | |
| case class Bid(hand: Hand, bid: Int) | |
| object Bid { | |
| given Ordering[Bid] with | |
| def compare(b1: Bid, b2: Bid): Int = | |
| Ordering[Hand].compare(b1.hand, b2.hand) | |
| extension (lb: List[Bid]) | |
| def totalWinnings: Int = lb.sorted.zipWithIndex.map { case (b, i) => | |
| b.bid * (i + 1) | |
| }.sum | |
| } | |
| case class BidWithJokers(original: Hand, upgraded: Hand, bid: Int) | |
| object BidWithJokers { | |
| import Card.* | |
| import Hand.* | |
| given Ordering[BidWithJokers] with | |
| def compare(b1: BidWithJokers, b2: BidWithJokers): Int = | |
| Ordering[Int].compare(b1.upgraded.ordinal, b2.upgraded.ordinal) match { | |
| case 0 => | |
| b1.original.cards | |
| .zip(b2.original.cards) | |
| .filterNot((c1, c2) => c1.value == c2.value) | |
| .headOption match { | |
| case Some((c1, c2)) => c1.value - c2.value | |
| case None => 0 | |
| } | |
| case x => x | |
| } | |
| extension (b: Bid) | |
| def toBidWithJokers: BidWithJokers = { | |
| val counts = b.hand.cards | |
| .groupBy(_.value) | |
| .view | |
| .mapValues(_.size) | |
| .toVector | |
| .sortBy(_._2) | |
| .reverse | |
| val jokers = counts.find(_._1 == -1) | |
| val notJokers = counts.filterNot(_._1 == -1) | |
| val upgraded = jokers match | |
| case None => b.hand // no jokers, upgraded is the Original | |
| case Some(_) if notJokers.isEmpty => | |
| FiveOfAKind(List.fill(5)(Ace)) // Only jokers, upgrade to Ace | |
| case Some(jokers) => | |
| val (cardV, previousBest) = notJokers.head | |
| val newCards = | |
| List.fill(jokers._2 + previousBest)(cardV.toCard).flatten | |
| val oldCards = b.hand.cards.filterNot(c => | |
| c.value == Joker.value || c.value == cardV | |
| ) | |
| val ret = newCards ++ oldCards | |
| assert(ret.size == 5, s"Should be 5 cards, got ${ret.size}") | |
| ret.toHand | |
| BidWithJokers(b.hand, upgraded, b.bid) | |
| } | |
| extension (lb: List[BidWithJokers]) | |
| def totalWinnings: Int = lb.sorted.zipWithIndex.map { case (b, i) => | |
| b.bid * (i + 1) | |
| }.sum | |
| } | |
| object Parse { | |
| import Day7.Card.* | |
| import Day7.Hand.* | |
| def hand(withJokers: Boolean): P[Option[Hand]] = | |
| octet.rep(5, 5).map { chars => | |
| chars.toList.flatMap(_.toCard(withJokers)) match { | |
| case h if h.size == 5 => h.toHand.some | |
| case _ => None | |
| } | |
| } | |
| def bid(withJokers: Boolean): P[Option[Bid]] = | |
| (hand(withJokers) ~ (wsp *> digits.map(_.toInt) <* lf.?)).map { | |
| case (Some(h), b) => Bid(h, b).some | |
| case _ => None | |
| } | |
| val all: P[List[Bid]] = bid(false).rep.map(_.toList.flatten) | |
| val allWithJokers: P[List[Bid]] = bid(true).rep.map(_.toList.flatten) | |
| } | |
| def part1F(i: String) = IO.delay { | |
| Parse.all.parseAll(i) match { | |
| case Right(bids) => bids.totalWinnings | |
| case Left(e) => IO.raiseError(new Throwable(s"Parse error\n${e.show}")) | |
| } | |
| } | |
| def part2F(i: String) = IO.delay { | |
| import BidWithJokers.* | |
| Parse.allWithJokers.parseAll(i) match { | |
| case Right(bids) => bids.map(_.toBidWithJokers).totalWinnings | |
| case Left(e) => IO.raiseError(new Throwable(s"Parse error\n${e.show}")) | |
| } | |
| } | |
| override def run: IO[Unit] = part1F(input).flatMap { r => | |
| IO.println(s"Result: $r") | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package net.tazato | |
| import weaver.* | |
| import cats.syntax.all.* | |
| object Day7Suite extends SimpleIOSuite { | |
| val example = | |
| """32T3K 765 | |
| |T55J5 684 | |
| |KK677 28 | |
| |KTJJT 220 | |
| |QQQJA 483""".stripMargin | |
| pureTest("Hand detector works") { | |
| import Day7.Card.* | |
| val h0 = List(Two, Two, Two, Two, Two) | |
| val p0 = Day7.Hand.fromCards(h0) | |
| expect.same(p0, Day7.Hand.FiveOfAKind(h0)) | |
| val h1 = List(Two, Two, Two, Two, Three) | |
| val p1 = Day7.Hand.fromCards(h1) | |
| expect.same(p1, Day7.Hand.FourOfAKind(h1)) | |
| val h2 = List(Two, Two, Two, Three, Three) | |
| val p2 = Day7.Hand.fromCards(h2) | |
| expect.same(p2, Day7.Hand.FullHouse(h2)) | |
| val h3 = List(Two, Two, Two, Three, Four) | |
| val p3 = Day7.Hand.fromCards(h3) | |
| expect.same(p3, Day7.Hand.ThreeOfAKind(h3)) | |
| val h4 = List(Two, Two, Three, Four, Four) | |
| val p4 = Day7.Hand.fromCards(h4) | |
| expect.same(p4, Day7.Hand.TwoPair(h4)) | |
| val h5 = List(Two, Two, Three, Four, Five) | |
| val p5 = Day7.Hand.fromCards(h5) | |
| expect.same(p5, Day7.Hand.OnePair(h5)) | |
| val h6 = List(Two, Three, Four, Five, Six) | |
| val p6 = Day7.Hand.fromCards(h6) | |
| expect.same(p6, Day7.Hand.HighCard(h6)) | |
| } | |
| pureTest("compare hands") { | |
| import Day7.Card.* | |
| import Day7.Hand.* | |
| import Day7.Hand.order.mkOrderingOps | |
| val h0 = List(Two, Two, Two, Two, Two).toHand | |
| val h1 = List(Two, Two, Two, Two, Three).toHand | |
| expect(h0 > h1) | |
| // The first card that is different is the one that matters | |
| val h2 = List(Two, Ace, Ace, Ace, Ace).toHand | |
| val h3 = List(Ace, Ace, Ace, Ace, Two).toHand | |
| expect(h2 < h3) | |
| val h4 = List(Two, Two, Two, Three, Three).toHand | |
| val h5 = List(Two, Two, Two, Four, Four).toHand | |
| expect(h4 < h5) | |
| val h6 = List(Two, Two, Two, Three, Four).toHand | |
| expect.eql(h6, h6) | |
| } | |
| pureTest("Day7 - Part 1 parse") { | |
| val bids = Day7.Parse.all.parseAll(example) | |
| bids match | |
| case Left(value) => failure(s"Failed to parse:\n${value.show}") | |
| case Right(value) => success | |
| } | |
| test("Day7 - Part 1 - example") { | |
| Day7.part1F(example) map { result => | |
| expect.same(result, 6440) | |
| } | |
| } | |
| test("Day7 - Part 1 - input") { | |
| Day7.part1F(Day7.input) map { result => | |
| expect.same(result, 251806792) | |
| } | |
| } | |
| pureTest("upgrade jokers works") { | |
| import Day7.BidWithJokers.* | |
| import Day7.Card.* | |
| import Day7.Hand.* | |
| val bid = Day7.Parse.bid(true).parseAll("T55J5 684").toOption.get.get | |
| val h0 = List(Ten, Five, Five, Joker, Five).toHand | |
| val upgrade = bid.toBidWithJokers | |
| expect.same(bid.hand, h0) | |
| } | |
| test("Day7 - Part 2 - example") { | |
| Day7.part2F(example) map { result => | |
| expect.same(result, 5905) | |
| } | |
| } | |
| test("Day7 - Part 2 - input") { | |
| Day7.part2F(Day7.input) map { result => | |
| expect.same(result, 252113488) | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment