Rope Bridge

Description: https://adventofcode.com/2022/day/9

open System
open System.IO

type Index(x: int, y: int) =
    member _.X = x
    member _.Y = y

    member this.Add(index: Index) =
        new Index(this.X + index.X, this.Y + index.Y)

    override this.ToString() = $"{this.X} {this.Y}"

    override this.Equals(index: obj) =
        match index with
        | :? Index as x -> this.X = x.X && this.Y = x.Y
        | _ -> false

    override this.GetHashCode() = HashCode.Combine(this.X, this.Y)

type Motion =
    | Left of int
    | Right of int
    | Up of int
    | Down of int

    static member parse(s: string) : Motion =
        let parts = s.Split ' '
        let direction = parts[0]
        let distance = parts[1] |> int

        match direction with
        | "L" -> Left distance
        | "R" -> Right distance
        | "D" -> Down distance
        | "U" -> Up distance
        | x -> failwith $"Invalid option {x}"

type Bridge(?hasNext: int) =
    let mutable headPosition = new Index(0, 0)
    let mutable tailPosition = new Index(0, 0)
    let mutable tailVisited: Index[] = [| tailPosition |]

    let next =

        let knots = defaultArg hasNext 0

        if knots > 0 then
            Some(new Bridge(hasNext = (knots - 1)))
        else
            None

    let notTouching () : bool =
        pown (tailPosition.X - headPosition.X) 2
        + pown (tailPosition.Y - headPosition.Y) 2 > 2

    let tailMove () : Index =
        let dirY = headPosition.Y - tailPosition.Y
        let dirX = headPosition.X - tailPosition.X
        let atan = Math.Atan2(dirY, dirX)
        let angle = (atan * (180.0 / Math.PI) + 360.0) % 360.0

        match angle with
        | 0.0 -> new Index(1, 0)
        | x when x > 0 && x < 90 -> new Index(1, 1)
        | 90.0 -> new Index(0, 1)
        | x when x > 90 && x < 180 -> new Index(-1, 1)
        | 180.0 -> new Index(-1, 0)
        | x when x > 180 && x < 270 -> new Index(-1, -1)
        | 270.0 -> new Index(0, -1)
        | x when x > 270 && x < 360 -> new Index(1, -1)
        | x -> failwith $"Unknown angle {x}"

    member _.TailVisited: Index[] =
        match next with
        | Some n -> n.TailVisited
        | None -> tailVisited

    member this.TailVisitedCount = this.TailVisited |> Array.distinct |> Array.length

    member this.PrintBridge() =
        let minX = this.TailVisited |> Array.minBy (fun x -> x.X)
        let maxX = this.TailVisited |> Array.maxBy (fun x -> x.X)
        let minY = this.TailVisited |> Array.minBy (fun x -> x.Y)
        let maxY = this.TailVisited |> Array.maxBy (fun x -> x.Y)

        for y in maxY.Y .. -1 .. minY.Y do
            for x in minX.X .. maxX.X do
                let contains = Array.contains (new Index(x, y)) this.TailVisited

                printf "%c" (if contains then '#' else '.')

            printfn ""

    member _.MoveHead(index: Index) =
        headPosition <- headPosition.Add(index)

        if notTouching () then
            let move = tailMove ()

            tailPosition <- tailPosition.Add(move)
            tailVisited <- Array.append tailVisited [| tailPosition |]

            match next with
            | Some b -> b.MoveHead(move)
            | None -> ()

    member this.Move(motion: Motion) =
        match motion with
        | Left x -> x, new Index(-1, 0)
        | Right x -> x, new Index(1, 0)
        | Up x -> x, new Index(0, 1)
        | Down x -> x, new Index(0, -1)
        |> (fun (count, direction) ->
            for _ in 1..count do
                this.MoveHead(direction))

type Input() =

    member _.Compute(part: int) =
        use stream = new StreamReader "./input.txt"
        let mutable endOfFile = false

        let bridge = new Bridge(hasNext = if part <> 1 then 8 else 0)

        while (not endOfFile) do
            let line = stream.ReadLine()

            if (line = null) then
                endOfFile <- true
            else
                Motion.parse line |> bridge.Move

        bridge.PrintBridge()
        bridge.TailVisitedCount

let input = Input()
let part = (Environment.GetCommandLineArgs()[1]) |> int

printfn "%d" (input.Compute(part))