AOC23 - 2 December
2023-12-2
Today is Saturday, however I have many fun plans with my girlfriend. So this post is going to be a short one, and I will only focus on the essentials. Let's get to it.
So let's copy my One folder and rename to Two, fire up elm reactor and ponder. I have a hunch that this is solvable by statistics. But maybe there is a smarter way I can find, once I modeled the data. Today I will begin with that. Everything else stays the same.
(but before actually doing any of the data modeling, I will first add a bit of styling to improve the user experience a notch)
view : Model -> Html Msg
view model =
div [style "display" "flex", style "flex-direction" "column", style "align-items" "center", style "margin" "40px"]
[ textarea
[style "width" "600px", style "height" "400px"
]
[]
, p [] [text ""]
]
Now my prompt area is centered and has some room to breath.
So, we have cubes.
- Cubes can be Red, Green and Blue.
- A bag gets loaded with a certain amount of cubes (uknown to me at the time)
- He will grab a few cubes and show them to me
- Resulting in a comma separated set of results
- Each set being separated by semicolons
type Cube = Red | Blue | Green
type alias CubeSet = Dict Cube Int
type alias Game = List CubeSet
The beauty of Data modeling in a Type strong functional language, is that you can precisely model the domain in a clear and concise way, no code line is wasted on any additional constructs. Also CubeCount is an alias for a tuple of Cubes and their amount, since I can't use my own type as key for a Dict. CubeSet defined like this is also abstract. It can both represent the entire Set of cubes loaded into the game, aswell as the sequence of Picks from the Elf inside one game.
To implement a custom type dictionary I had to use a library, which changes the way we update our set:
...
import AssocList as Dict exposing (Dict)
...
update : k -> (Maybe v -> Maybe v) -> Dict k v -> Dict k v
Now we need to parse the input.
A UI reflecting the problem
But first let me read the challenge again. Ok so we have a long list of games as main input.
We then need a way to input the Set of Cubes available. I will make a counter for each color.
...
type Msg = OnIncrement Cube | OnDecrement Cube | OnPromptUpdate String
type alias Model =
{loadedCubes : CubeSet
, gameList : String
}
...
init : Model
init =
{ loadedCubes = Dict.fromList [(Red, 0), (Green, 0), (Blue, 0)]
, gameList = ""
}
view : Model -> Html Msg
view model =
div [style "display" "flex", style "flex-direction" "column", style "align-items" "center", style "margin" "40px"]
[ textarea
[style "width" "600px", style "height" "400px"
, onInput OnPromptUpdate
]
[text model.gameList]
, div [style "display" "flex", style "flex-direction" "row"]
[cubeCounter model Red
, cubeCounter model Blue
, cubeCounter model Green
]
, p [] [text ""]
]
writeColor : Cube -> String
writeColor color =
case color of
Red -> "RED"
Blue -> "BLUE"
Green -> "GREEN"
readColor : String -> Maybe Cube
readColor string =
case string of
"red" -> Just Red
"green" -> Just Green
"blue" -> Just Blue
_ -> Nothing
cubeCounter : Model -> Cube -> Html Msg
cubeCounter model color =
div []
[ div [style "display" "flex", style "flex-direction" "column", style "margin" "20px"]
[ button [onClick <| OnIncrement color] [text "+"]
, div [style "margin" "20px"] [text (writeColor color ++ " :" ++ (String.fromInt <| getCountForColor model.loadedCubes color))]
, button [onClick <| OnDecrement color] [text "-"]
]
]
getCountForColor : CubeSet -> Cube -> Int
getCountForColor set color = (Maybe.withDefault 0 <| Dict.get color set)
update : Msg -> Model -> Model
update msg model =
case msg of
OnDecrement color -> changeCubeCount color False model
OnIncrement color -> changeCubeCount color True model
OnPromptUpdate a -> {model | gameList = a}
changeCubeCount : Cube -> Bool -> Model -> Model
changeCubeCount color up model =
{model | loadedCubes =
Dict.update color
(\a -> Just ( Maybe.withDefault 0 a + (if up then 1 else -1)))
model.loadedCubes
}
Having an updated UI reflecting my Problem, I am off to have a christmassy day with girlfriend and friends.
To maybe hell and back
I am back. It is late, and I didn't really have the time or right headspace to reflect anything. I started by parsing the input and started to lose myself in a messy Maybe hell (which I am sure could be resolved more elegantly), but certainly not at 1am.
processCode : Model -> String -> Int
processCode model change =
String.lines change |>
List.map parseGame |>
(List.filter (possibleGame model.loadedCubes)) |>
(List.map .id) |>
List.sum
possibleGame : CubeSet -> Game -> Bool
possibleGame cubeset game = True
parseGame : String -> Game
parseGame string =
let
queryGameId =
Maybe.withDefault Regex.never <|
Regex.fromString "(?<=Game\\s)\\d*"
gameSetList = Array.get 1 (Array.fromList <| String.split ":" string)
matchedId = Maybe.withDefault
{ match = ""
, index = 0
, number = 0
, submatches = []
} <| List.head <| Debug.log "Regex match: " Regex.find queryGameId string
in
{ sets = List.map parseSetList (String.split ";" <| Maybe.withDefault "" gameSetList)
, id = matchedId.match |> String.toInt >> Maybe.withDefault 0
}
parseSetList : String -> CubeSet
parseSetList string = Dict.fromList <|
(String.split "," string |> List.map ((String.dropLeft 1) >> resolveCubeAmount) |> Maybe.Extra.values)
resolveCubeAmount : String -> Maybe (Cube, Int)
resolveCubeAmount string =
let
values = Array.fromList <| String.split " " string
result = (readColor (Maybe.withDefault "" <| Array.get 1 values), String.toInt <| Maybe.withDefault "" <| Array.get 0 values)
in traverseMaybe result
traverseMaybe : (Maybe a, Maybe b) -> Maybe (a, b)
traverseMaybe tuple =
case tuple of
(Just a, Just b) -> Just (a, b)
(Nothing, _) -> Nothing
(_, Nothing) -> Nothing
Essentially what I did, is similar to day 1, parsed the text to lines, mapped over them with parseGame, filter for possibleGames, get the id of each game, and sum the ids. Game parse is a matter of matching "Game xx:" which can be done easily with a positive lookbehind and then with multiple string splitting I could dissect ";" into sets with each entry being separated by ",". The rest is Maybe handling and String cleanup. The result is a Game with an Id and a list of cube sets.
Now to the big question:
Is the game Possible?
Now initially I thought I had to do a statistical evaluation of the likelihood of the given picks with the given input cubes. Turns out though, that I just need to check for each of the given cube in my total cubes if any of the sets in the game have a bigger amount and declare them physically impossible.
possibleGame : CubeSet -> Game -> Bool
possibleGame cubeset game = Dict.toList cubeset |>
List.any
( \a ->
(collectCubesOfColor game (Tuple.first a) |>
List.maximum |>
Maybe.withDefault 0) > (Tuple.second a)) |>
not
collectCubesOfColor : Game -> Cube -> List Int
collectCubesOfColor game cube = List.map (Dict.get cube >> Maybe.withDefault 0) game.sets
This solution reads like plain english. A possible game is any game where none of the contained set's cube amount is higher than the input cubes of that color. To do that we iterate over each Cube amount pair of the input cubes, collect the same cubes of that color from each of the game's sets, get the maximum and check if it is higher than the target count.
Unfortunately, I am dead tired and need to sleep. So today there will be no part 2. But this will be an exception. See you on the next Chapter!
A few days later
Well today somehow I got everything done pretty quickly and day 6 was surprisingly easy, so here I am. I had a look at it and took a gander.
solve2 : Model -> String -> Int
solve2 model change =
String.lines change |>
List.map parseGame |>
List.map minimalSet |>
List.map List.product |>
List.sum
minimalSet : Game -> List Int
minimalSet game =
[Red, Blue, Green] |>
List.map (collectCubesOfColor game) |>
List.map List.maximum |>
Maybe.values
Instead of collecting the games I map over all games with minimalSet, which collects one after another a list of all Red, Blue and Green cubes in a game gets the max, the result is then multiplied and summed up for each game.
Well, that was easy. Should have at least read it before dismissing it. But the completionist in me won't let you down :).