Skip to content

Instantly share code, notes, and snippets.

@CJSmith-0141
Created December 11, 2023 01:06
Show Gist options
  • Select an option

  • Save CJSmith-0141/e05da724e4d4ae41c689122fc51cc083 to your computer and use it in GitHub Desktop.

Select an option

Save CJSmith-0141/e05da724e4d4ae41c689122fc51cc083 to your computer and use it in GitHub Desktop.
Aoc 2023 Day 7
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")
}
}
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