🏡 index : ~doyle/aoc.git

#!/usr/bin/env nix-shell
#!nix-shell -i runghc -p "haskellPackages.ghcWithPackages (pkgs: with pkgs; [ ])"

import Data.List (intersect)
import Text.Parsec
import Text.Parsec.Char
import Text.Parsec.Combinator
import Text.Parsec.String (Parser)

{- https://adventofcode.com/2023/day/4 -}

main = do
  cards <- readAndParseStdin []
  print $ part1 cards
  print $ part2 cards

-- sum up amount of winning numbers using the part1Score formula
part1 :: [Card] -> Int
part1 cards = sum $ map (score . length . getWinningNumbers) cards
  where
    score 0 = 0
    score n = 2 ^ (n - 1)

-- calculates number of cards won in part 2 of the task
part2 :: [Card] -> Int
part2 cards = sum $ calculateCardCopies $ map (length . getWinningNumbers) cards

-- starts with a base array of [1; n] and replicates Ns right for each `getWinningNumbers`
-- where N is the amount of card replicas
calculateCardCopies :: [Int] -> [Int]
calculateCardCopies xs = foldl replicateSingleCardWinnings (replicate (length xs) 1) (zip [0 ..] xs)

-- helper function for calling `copyCards` within `foldl`
replicateSingleCardWinnings :: [Int] -> (Int, Int) -> [Int]
replicateSingleCardWinnings cardReplicas (idx, winningNumbers) = copyCards cardReplicas (idx + 1) idx winningNumbers

-- copies N cards to `winningNumbers` elements right of `winningCardIdx` where N is `cardReplicas[winningCardIdx]`
copyCards :: [Int] -> Int -> Int -> Int -> [Int]
copyCards cardReplicas currIdx winningCardIdx winningNumbers
  | currIdx <= length cardReplicas && winningNumbers > 0 =
      let incrementedList = incrementAtIndex cardReplicas currIdx (cardReplicas !! winningCardIdx)
       in copyCards incrementedList (currIdx + 1) winningCardIdx (winningNumbers - 1)
  | otherwise = cardReplicas

-- takes a list, an index and an amount to increment by
incrementAtIndex :: [Int] -> Int -> Int -> [Int]
incrementAtIndex xs idx amount = take idx xs ++ [(xs !! idx) + amount] ++ drop (idx + 1) xs

-- gets the intersection of winning numbers and player numbers
getWinningNumbers :: Card -> [Int]
getWinningNumbers card = myNumbers card `intersect` winningNumbers card

data Card = Card
  { winningNumbers :: [Int],
    myNumbers :: [Int]
  }
  deriving (Show)

-- reads entirety of stdin and parses each line
readAndParseStdin :: [Card] -> IO [Card]
readAndParseStdin acc = do
  line <- getLine
  if null line
    then return acc
    else case parse cardParser "" line of
      Left parseError -> error $ show parseError
      Right card -> readAndParseStdin $ acc ++ [card]

-- parses a `Card [i]: [n1] [n2] [n3] | [n4] [n5] [n6]` line
cardParser :: Parser Card
cardParser = do
  _ <- string "Card" <* spaces <* many1 digit <* char ':' <* spaces
  winningNumbers <- numberParser
  _ <- char '|' <* spaces
  myNumbers <- numberParser

  return Card {winningNumbers, myNumbers}

-- reads a single number delimited by spaces
numberParser :: Parser [Int]
numberParser = map read <$> many1 digit `endBy` spaces