Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Advent of Code 2024 - Day 16

2024/elixir/livebooks/day16.livemd

Advent of Code 2024 - Day 16

Mix.install([
  {:kino_aoc, "~> 0.1"}
])

Introduction

— Day 16: Reindeer Maze —

It’s time again for the Reindeer Olympics! This year, the big event is the Reindeer Maze, where the Reindeer compete for the lowest score.

You and The Historians arrive to search for the Chief right as the event is about to start. It wouldn’t hurt to watch a little, right?

The Reindeer start on the Start Tile (marked S) facing East and need to reach the End Tile (marked E). They can move forward one tile at a time (increasing their score by 1 point), but never into a wall (#). They can also rotate clockwise or counterclockwise 90 degrees at a time (increasing their score by 1000 points).

To figure out the best place to sit, you start by grabbing a map (your puzzle input) from a nearby kiosk. For example:

###############
#.......#....E#
#.#.###.#.###.#
#.....#.#...#.#
#.###.#####.#.#
#.#.#.......#.#
#.#.#####.###.#
#...........#.#
###.#.#####.#.#
#...#.....#.#.#
#.#.#.###.#.#.#
#.....#...#.#.#
#.###.#.#.#.#.#
#S..#.....#...#
###############

There are many paths through this maze, but taking any of the best paths would incur a score of only 7036. This can be achieved by taking a total of 36 steps forward and turning 90 degrees a total of 7 times:

###############
#.......#....E#
#.#.###.#.###^#
#.....#.#...#^#
#.###.#####.#^#
#.#.#.......#^#
#.#.#####.###^#
#..>>>>>>>>v#^#
###^#.#####v#^#
#>>^#.....#v#^#
#^#.#.###.#v#^#
#^....#...#v#^#
#^###.#.#.#v#^#
#S..#.....#>>^#
###############

Here’s a second example:

#################
#...#...#...#..E#
#.#.#.#.#.#.#.#.#
#.#.#.#...#...#.#
#.#.#.#.###.#.#.#
#...#.#.#.....#.#
#.#.#.#.#.#####.#
#.#...#.#.#.....#
#.#.#####.#.###.#
#.#.#.......#...#
#.#.###.#####.###
#.#.#...#.....#.#
#.#.#.#####.###.#
#.#.#.........#.#
#.#.#.#########.#
#S#.............#
#################

In this maze, the best paths cost 11048 points; following one such path would look like this:

#################
#...#...#...#..E#
#.#.#.#.#.#.#.#^#
#.#.#.#...#...#^#
#.#.#.#.###.#.#^#
#>>v#.#.#.....#^#
#^#v#.#.#.#####^#
#^#v..#.#.#>>>>^#
#^#v#####.#^###.#
#^#v#..>>>>^#...#
#^#v###^#####.###
#^#v#>>^#.....#.#
#^#v#^#####.###.#
#^#v#^........#.#
#^#v#^#########.#
#S#>>^..........#
#################

Note that the path shown above includes one 90 degree turn as the very first move, rotating the Reindeer from facing East to facing North.

Analyze your map carefully. What is the lowest score a Reindeer could possibly get?

— Part Two —

Description

Puzzle

{:ok, puzzle_input} =
  KinoAOC.download_puzzle("2024", "16", System.fetch_env!("LB_AOC_SESSION"))
IO.puts(puzzle_input)

Tools

Tools - Code

defmodule Matrix do
  def size(matrix) do
    rows = matrix |> length()
    cols = matrix |> hd() |> length()

    {rows, cols}
  end

  def value(matrix, {x, y}) do
    {rows, cols} = size(matrix)

    if x >= 0 and x < cols and (y >= 0 and y < rows) do
      matrix
      |> Enum.at(y, [])
      |> Enum.at(x)
    else
      nil
    end
  end
end

defmodule NodeBase do
  defstruct g: 0,
            h: 0,
            f: 0,
            point: {0, 0},
            from: {0, 0},
            neighbors: [],
            direction: ""
end

Parser

Code - Parser

defmodule Parser do
  def parse(input) do
    input
    |> String.split("\n", trim: true)
    |> Enum.map(&amp;String.codepoints(&amp;1))
  end
end

Tests - Parser

ExUnit.start(autorun: false)

defmodule ParserTest do
  use ExUnit.Case, async: true
  import Parser

  @input """
  ###############
  #.......#....E#
  #.#.###.#.###.#
  #.....#.#...#.#
  #.###.#####.#.#
  #.#.#.......#.#
  #.#.#####.###.#
  #...........#.#
  ###.#.#####.#.#
  #...#.....#.#.#
  #.#.#.###.#.#.#
  #.....#...#.#.#
  #.###.#.#.#.#.#
  #S..#.....#...#
  ###############
  """
  @expected [
    ["#", "#", "#", "#", "#", "#", "#", "#", "#", "#", "#", "#", "#", "#", "#"],
    ["#", ".", ".", ".", ".", ".", ".", ".", "#", ".", ".", ".", ".", "E", "#"],
    ["#", ".", "#", ".", "#", "#", "#", ".", "#", ".", "#", "#", "#", ".", "#"],
    ["#", ".", ".", ".", ".", ".", "#", ".", "#", ".", ".", ".", "#", ".", "#"],
    ["#", ".", "#", "#", "#", ".", "#", "#", "#", "#", "#", ".", "#", ".", "#"],
    ["#", ".", "#", ".", "#", ".", ".", ".", ".", ".", ".", ".", "#", ".", "#"],
    ["#", ".", "#", ".", "#", "#", "#", "#", "#", ".", "#", "#", "#", ".", "#"],
    ["#", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "#", ".", "#"],
    ["#", "#", "#", ".", "#", ".", "#", "#", "#", "#", "#", ".", "#", ".", "#"],
    ["#", ".", ".", ".", "#", ".", ".", ".", ".", ".", "#", ".", "#", ".", "#"],
    ["#", ".", "#", ".", "#", ".", "#", "#", "#", ".", "#", ".", "#", ".", "#"],
    ["#", ".", ".", ".", ".", ".", "#", ".", ".", ".", "#", ".", "#", ".", "#"],
    ["#", ".", "#", "#", "#", ".", "#", ".", "#", ".", "#", ".", "#", ".", "#"],
    ["#", "S", ".", ".", "#", ".", ".", ".", ".", ".", "#", ".", ".", ".", "#"],
    ["#", "#", "#", "#", "#", "#", "#", "#", "#", "#", "#", "#", "#", "#", "#"]
  ]

  test "parse test" do
    assert parse(@input) == @expected
  end
end

ExUnit.run()

Part One

Code - Part 1

defmodule PartOne do
  def solve(input) do
    IO.puts("--- Part One ---")
    IO.puts("Result: #{run(input)}")
  end

  def run(input) do
    matrix =
      input
      |> Parser.parse()

    {rows, cols} = Matrix.size(matrix)

    map =
      for y <- 0..(rows - 1), x <- 0..(cols - 1), reduce: %{} do
        map ->
          case Matrix.value(matrix, {x, y}) do
            "#" ->
              map

            value ->
              Map.put(map, {x, y}, %{value: value, current: {0, 0}, g: 0, h: 0, dir: ""})
          end
      end

    {start_point, target_point} =
      map
      |> Enum.reduce({{0, 0}, {0, 0}}, fn {point, %{value: value}}, {start_point, target_point} ->
        case value do
          "S" -> {point, target_point}
          "E" -> {start_point, point}
          _ -> {start_point, target_point}
        end
      end)

    first_node = create_node(map, start_point, target_point, nil, start_node)

    path = pathfind(map, target_point, [first_node], [])

    # start_point =
    #   map
    #   |> Enum.find(fn {_, value} -> value == "S" end)
    #   |> elem(0)

    # end_point =
    #   map
    #   |> Enum.find(fn {_, value} -> value == "E" end)
    #   |> elem(0)
  end

  defp distance({x1, y1}, {x2, y2}) do
    abs(x1 - x2) + abs(y1 - y2)
  end

  def calculate_g(from_point, current_point) do
    
  end

  def create_node(map, start, target, from, current) do
    g = distance(point, start)
    h = distance(point, target)
    f = g + h

    neighbors =
      [{x, y - 1}, {x, y + 1}, {x - 1, y}, {x + 1, y}]
      |> Enum.filter(fn p -> Map.get(map, p) != nil end)

    %NodeBase{
      g: g,
      h: h,
      f: f,
      point: point,
      from: from,
      neighbors: neighbors
    }
  end

  def sort_nodes(nodes) do
    nodes
    |> Enum.sort(fn node1, node2 -> f(node1) < f(node2) end)
  end

  def pathfind(map, end_point, open, closed) do
    if end_point == point do
      closed
    else
      up = Map.get(map, {x, y - 1})
      down = Map.get(map, {x, y + 1})
      left = Map.get(map, {x - 1, y})
      right = Map.get(map, {x + 1, y})
    end
  end
end

Tests - Part 1

ExUnit.start(autorun: false)

defmodule PartOneTest do
  use ExUnit.Case, async: true
  import PartOne

  @input """
  ###############
  #.......#....E#
  #.#.###.#.###.#
  #.....#.#...#.#
  #.###.#####.#.#
  #.#.#.......#.#
  #.#.#####.###.#
  #...........#.#
  ###.#.#####.#.#
  #...#.....#.#.#
  #.#.#.###.#.#.#
  #.....#...#.#.#
  #.###.#.#.#.#.#
  #S..#.....#...#
  ###############
  """
  @expected nil

  test "part one" do
    assert run(@input) == @expected
  end
end

ExUnit.run()

Solution - Part 1

PartOne.solve(puzzle_input)

Part Two

Code - Part 2

defmodule PartTwo do
  def solve(input) do
    IO.puts("--- Part Two ---")
    IO.puts("Result: #{run(input)}")
  end

  def run(input) do
  end
end

Tests - Part 2

ExUnit.start(autorun: false)

defmodule PartTwoTest do
  use ExUnit.Case, async: true
  import PartTwo

  @input ""
  @expected nil

  test "part two" do
    assert run(@input) == @expected
  end
end

ExUnit.run()

Solution - Part 2

PartTwo.solve(puzzle_input)