Advent Of Code 2022 - Day 02

Dec 02, 2022

It's December. Time for snow, slippery roads, hot chocolate and cozy fire places. Also time for Advent of Code. An advent calendar with small, daily programming puzzles, growing progressively more difficult.

Every year I participate in a programming language I did not use for Advent of Code before, in order to learn new ways of doing things and to challenge myself. This year, that language is F#.

Day 02: Rock Paper Scissors

Summary: Given a list of games of Rock Paper Scissors, determine who wins. Award points of choice for Rock (1), Paper (2) or Scissors (3) and for winning (6), drawing (3) or losing (0). Give the sum of the points of all the games.

Input is given as two characters per line. The first character represents the opponent's choice, the second one the player's. The first character is one of "A" (Rock), "B" (Paper) or "C" (Scissors). The second is one of "X" (Rock), "Y" (Paper) or "Z" (Scissors).

A Y
B X
C Z

Find the full description here.

Taking a hint from yesterday's reflection that an imperative solution that perhaps cuts some corners never crossed my mind, today I went for a very dirty imperative solution, hard coding where I can.

The game of Rock Paper Scissors has nine possible outcomes.

Below plays against right Rock Paper Scissors
Rock Draw Loss Win
Paper Win Draw Loss
Scissors Loss Win Draw

This means we can loop over the lines, hardcode every possible input and add the scores together.

let solve1 (input: string list) =
    let mutable score = 0
    for line in input do
        match line.Split(" ") with
        | [|"A"; "X"|] -> score <- score + 1 + 3
        | [|"A"; "Y"|] -> score <- score + 2 + 6
        | [|"A"; "Z"|] -> score <- score + 3 + 0
        | [|"B"; "X"|] -> score <- score + 1 + 0
        | [|"B"; "Y"|] -> score <- score + 2 + 3
        | [|"B"; "Z"|] -> score <- score + 3 + 6
        | [|"C"; "X"|] -> score <- score + 1 + 6
        | [|"C"; "Y"|] -> score <- score + 2 + 0
        | [|"C"; "Z"|] -> score <- score + 3 + 3
        | _ -> failwith "Impossible move"
    score

Part 2

Summary: It turns out that "X", "Y" and "Z" means something different. It means that we have to Lose ("X"), Draw ("Y") or Win ("Z") against the opponent's move.

Had I used a different strategy to implement the first part, part two may have required some new logic. However, despite the meaning of "X", "Y" and "Z" changing, the game still has the same nine states. We just have to look at a different dimension.

let solve2 (input: string list) =
    let mutable score = 0
    for line in input do
        match line.Split(" ") with
        | [|"A"; "X"|] -> score <- score + 3 + 0
        | [|"A"; "Y"|] -> score <- score + 1 + 3
        | [|"A"; "Z"|] -> score <- score + 2 + 6
        | [|"B"; "X"|] -> score <- score + 1 + 0
        | [|"B"; "Y"|] -> score <- score + 2 + 3
        | [|"B"; "Z"|] -> score <- score + 3 + 6
        | [|"C"; "X"|] -> score <- score + 2 + 0
        | [|"C"; "Y"|] -> score <- score + 3 + 3
        | [|"C"; "Z"|] -> score <- score + 1 + 6
        | _ -> failwith "Impossible move"
    score

Improvements

I thought long and hard about if I want to change anything. Maybe add types for the choices and outcomes. In the end that may look nicer but doesn't really add something.

Despite that, there are still two improvements to be made. First of all, splitting the line and matching the resulting array is not necessary. We can just match any of the nine possible input strings.

Second is the mutation. After scoring the stars I do want to switch to a more functional style.

Combining both improvements, the solution looks like this:

let solve1 (input: string list) =
    List.fold (fun score (line: string) ->
        match line with
        | "A X" -> score + 1 + 3
        | "A Y" -> score + 2 + 6
        | "A Z" -> score + 3 + 0
        | "B X" -> score + 1 + 0
        | "B Y" -> score + 2 + 3
        | "B Z" -> score + 3 + 6
        | "C X" -> score + 1 + 6
        | "C Y" -> score + 2 + 0
        | "C Z" -> score + 3 + 3
        | _ -> failwith "Impossible move") 0 input

Reflection

Solving today's problem quick and dirty was definitely faster than if I hard tried with with List.fold immediately, or if I had added types for both choice and outcome, and small functions that determine the outcome based on choice as well as the other way around.

It feels a bit conflicted. On the one hand there's the competetive aspect of Advent of Code for which this is completely fine. On the other hand there's the goal of writing code in a programming languange that I don't use very often. Part of that goal is making that language's good practices my own.

Those two aspects collide here, as I suspect they will continue to do throughout the next problems.