Day 7: Camel Cards

Megathread guidelines

  • Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
  • Code block support is not fully rolled out yet but likely will be in the middle of the event. Try to share solutions as both code blocks and using something such as https://topaz.github.io/paste/ , pastebin, or github (code blocks to future proof it for when 0.19 comes out and since code blocks currently function in some apps and some instances as well if they are running a 0.19 beta)

FAQ


🔒 Thread is locked until there’s at least 100 2 star entries on the global leaderboard

🔓 Thread has been unlocked after around 20 mins

  • @[email protected]
    link
    fedilink
    English
    211 months ago

    [language: Lean4]

    As with the previous days: I’ll only post the solution and parsing, not the dependencies I’ve put into separate files. For the full source code, please see github.

    The key idea for part 2 was that

    Spoiler

    it doesn’t make any sense to pick different cards for the jokers, and that it’s always the highest score to assign all jokers to the most frequent card.

    Solution
    inductive Card
      | two
      | three
      | four
      | five
      | six
      | seven
      | eight
      | nine
      | ten
      | jack
      | queen
      | king
      | ace
      deriving Repr, Ord, BEq
    
    inductive Hand
      | mk : Card → Card → Card → Card → Card → Hand
      deriving Repr, BEq
    
    private inductive Score
      | highCard
      | onePair
      | twoPair
      | threeOfAKind
      | fullHouse
      | fourOfAKind
      | fiveOfAKind
      deriving Repr, Ord, BEq
    
    -- we need countCards in part 2 again, but there it has different types
    private class CardList (η : Type) (χ : outParam Type) where
      cardList : η → List χ
    
    -- similarly, we can implement Ord in terms of CardList and Score
    private class Scorable (η : Type) where
      score : η → Score
    
    private instance : CardList Hand Card where
      cardList := λ
        | .mk a b c d e => [a,b,c,d,e]
    
    private def countCards {η χ : Type} (input :η) [CardList η χ] [Ord χ] [BEq χ] : List (Nat × χ) :=
      let ordered := (CardList.cardList input).quicksort
      let helper := λ (a : List (Nat × χ)) (c : χ) ↦ match a with
      | [] => [(1, c)]
      | a :: as =>
        if a.snd == c then
          (a.fst + 1, c) :: as
        else
          (1, c) :: a :: as
      List.quicksortBy (·.fst > ·.fst) $ ordered.foldl helper []
    
    private def evaluateCountedCards : (l : List (Nat × α)) → Score
      | [_] => Score.fiveOfAKind -- only one entry means all cards are equal
      | (4,_) :: _ => Score.fourOfAKind
      | [(3,_), (2,_)] => Score.fullHouse
      | (3,_) :: _ => Score.threeOfAKind
      | [(2,_), (2,_), _] => Score.twoPair
      | (2,_) :: _ => Score.onePair
      | _ => Score.highCard
    
    private def Hand.score (hand : Hand) : Score :=
      evaluateCountedCards $ countCards hand
    
    private instance : Scorable Hand where
      score := Hand.score
    
    instance {σ χ : Type} [Scorable σ] [CardList σ χ] [Ord χ] : Ord σ where
      compare (a b : σ) :=
        let comparedScores := Ord.compare (Scorable.score a) (Scorable.score b)
        if comparedScores != Ordering.eq then
          comparedScores
        else
          Ord.compare (CardList.cardList a) (CardList.cardList b)
    
    private def Card.fromChar? : Char → Option Card
    | '2' => some Card.two
    | '3' => some Card.three
    | '4' => some Card.four
    | '5' => some Card.five
    | '6' => some Card.six
    | '7' => some Card.seven
    | '8' => some Card.eight
    | '9' => some Card.nine
    | 'T' => some Card.ten
    | 'J' => some Card.jack
    | 'Q' => some Card.queen
    | 'K' => some Card.king
    | 'A' => some Card.ace
    | _ => none
    
    private def Hand.fromString? (input : String) : Option Hand :=
      match input.toList.mapM Card.fromChar? with
      | some [a, b, c, d, e] => Hand.mk a b c d e
      | _ => none
    
    abbrev Bet := Nat
    
    structure Player where
      hand : Hand
      bet : Bet
      deriving Repr
    
    def parse (input : String) : Except String (List Player) := do
      let lines := input.splitOn "\n" |> List.map String.trim |> List.filter String.notEmpty
      let parseLine := λ (line : String) ↦
        if let [hand, bid] := line.split Char.isWhitespace |> List.map String.trim |> List.filter String.notEmpty then
          Option.zip (Hand.fromString? hand) (String.toNat? bid)
          |> Option.map (uncurry Player.mk)
          |> Option.toExcept s!"Line could not be parsed: {line}"
        else
          throw s!"Failed to parse. Line did not separate into hand and bid properly: {line}"
      lines.mapM parseLine
    
    def part1 (players : List Player) : Nat :=
      players.quicksortBy (λ p q ↦ p.hand < q.hand)
      |> List.enumFrom 1
      |> List.foldl (λ r p ↦ p.fst * p.snd.bet + r) 0
    
    
    ------------------------------------------------------------------------------------------------------
    -- Again a riddle where part 2 needs different data representation, why are you doing this to me? Why?
    -- (Though, strictly speaking, I could just add "joker" to the list of cards in part 1 and treat it special)
    
    private inductive Card2
      | joker
      | two
      | three
      | four
      | five
      | six
      | seven
      | eight
      | nine
      | ten
      | queen
      | king
      | ace
      deriving Repr, Ord, BEq
    
    private def Card.toCard2 : Card → Card2
      | .two => Card2.two
      | .three => Card2.three
      | .four => Card2.four
      | .five => Card2.five
      | .six => Card2.six
      | .seven => Card2.seven
      | .eight => Card2.eight
      | .nine => Card2.nine
      | .ten => Card2.ten
      | .jack => Card2.joker
      | .queen => Card2.queen
      | .king => Card2.king
      | .ace => Card2.ace
    
    private inductive Hand2
      | mk : Card2 → Card2 → Card2 → Card2 → Card2 → Hand2
      deriving Repr
    
    private def Hand.toHand2 : Hand → Hand2
      | Hand.mk a b c d e => Hand2.mk a.toCard2 b.toCard2 c.toCard2 d.toCard2 e.toCard2
    
    instance : CardList Hand2 Card2 where
      cardList := λ
        | .mk a b c d e => [a,b,c,d,e]
    
    private def Hand2.score (hand : Hand2) : Score :=
      -- I could be dumb here and just let jokers be any other card, but that would be really wasteful
      -- Also, I'm pretty sure there is no combination that would benefit from jokers being mapped to
      -- different cards.
      -- and, even more important, I think we can always map jokers to the most frequent card and are
      -- still correct.
      let counted := countCards hand
      let (jokers, others) := counted.partition λ e ↦ e.snd == Card2.joker
      let jokersReplaced := match jokers, others with
      | (jokers, _) :: _ , (a, ac) :: as => (a+jokers, ac) :: as
      | _ :: _, [] => jokers
      | [], others => others
      evaluateCountedCards jokersReplaced
    
    private instance : Scorable Hand2 where
      score := Hand2.score
    
    private structure Player2 where
      bet : Bet
      hand2 : Hand2
    
    def part2 (players : List Player) : Nat :=
      let players := players.map λ p ↦
        {bet := p.bet, hand2 := p.hand.toHand2 : Player2}
      players.quicksortBy (λ p q ↦ p.hand2 < q.hand2)
      |> List.enumFrom 1
      |> List.foldl (λ r p ↦ p.fst * p.snd.bet + r) 0