Setting up for Advent of Code 2025

AOC 2025 is right around the corner. Last year, my first year, I just started a raw Elixir application and manually created modules for each day’s challenge. This year I’m thinking of starting with the aoc package. Anyone have opinions about what the most helpful framework is for enjoying this year’s challenges?

1 Like

Here’s a similar thread, which also contains my opinion :grin:

2 Likes

The aoc package is opinionated, it creates modules with the parse/2, part_one/1 and part_two/2 functions, and it creates tests if you fancy some TDD. If you do not like that, you can still use it just to fetch the inputs locally.

If you want to do your setup yourself I guess starting with a bash script that generates a module can be enough.

I’d suggest to try aoc right now, see the Are you trying this library before December 1st ? section so you know if it is useful to you before day 1.

This is my bash command to work with my package that I run every morning:

play:
  mix aoc.create | rg '\.ex' | rg -v Compiling | xargs code
  mix format
  nohup firefox $(mix aoc.url | rg "https") >/dev/null 2>&1 &
  echo 'Good Luck :)'
4 Likes

Frankly, with LLMs these days, you don’t necessarily need to rely on external packages for simple things - you can just ask the LLM of your choice to write your own little modules (or even write them yourself!).

My approach: 1) a module to download and cache the input files, called like this: AOC.input!(1, 2015) (first call downloads, consequtive calls read from the disk), 2) two simple templates called lib/y2015/day00.ex and test/y2015/day00_test.exs, 3) a mix tasks that would copy this tasks in appropriate places and change 00 to whatever the proper date is. So that I can modify the templates for my liking - they are just elixir files.

That’s it. That’s the whole setup.

# lib/aoc.ex
defmodule AOC do
  @year 2025
  @ua "Victor Kryukov (victor.kryukov@gmail.com; Elixir/Req)"
  @input_dir "priv/aoc"

  def session! do
    ".aoc_session"
    |> File.read!()
    |> String.trim()
  end

  def client(session \\ session!()) do
    Req.new(
      base_url: "https://adventofcode.com",
      headers: [
        {"cookie", "session=#{session}"},
        {"user-agent", @ua}
      ],
      redirect: true
    )
  end

  def input!(day, year \\ @year) do
    dir = @input_dir
    path = input_path(day, year, dir)

    cond do
      File.exists?(path) -> read_input_file!(path)
      true -> fetch_and_store_input!(day, year, path, dir)
    end
  end

  def save_input!(day, dir \\ @input_dir, year \\ @year) do
    path = input_path(day, year, dir)
    File.mkdir_p!(dir)
    File.write!(path, input!(day, year) <> "\n")
    path
  end

  defp fetch_and_store_input!(day, year, path, dir) do
    body = fetch_remote_input!(day, year)
    File.mkdir_p!(dir)
    File.write!(path, body <> "\n")
    body
  end

  defp fetch_remote_input!(day, year) do
    remote_path = "/#{year}/day/#{day}/input"

    case Req.get(client(), url: remote_path) do
      {:ok, %{status: 200, body: body}} -> String.trim_trailing(body)
      {:ok, %{status: 400}} -> raise "Bad request (likely missing/invalid session)"
      {:ok, %{status: 404}} -> raise "Not available yet (unlocks midnight ET Dec 1–25)"
      {:ok, resp} -> raise "HTTP #{resp.status} for #{remote_path}"
      {:error, err} -> raise err
    end
  end

  defp input_path(day, year, dir) do
    name = "#{year}_day#{day |> Integer.to_string() |> String.pad_leading(2, "0")}.txt"
    Path.join(dir, name)
  end

  defp read_input_file!(path) do
    path
    |> File.read!()
    |> String.trim_trailing()
  end
end
# lib/y2015/day00.ex
defmodule Y2015.Day00 do
  def part1(_s) do
  end

  def part2(_s) do
  end
end
# test/y2015/day00_test.exs
defmodule Y2015.Day00Test do
  use ExUnit.Case

  import Y2015.Day00

  @tag :skip
  test "part1/1" do
    assert part1(AOC.input!(0, 2015))
  end

  @tag :skip
  test "part2/1" do
    assert part2(AOC.input!(0, 2015))
  end
end
# mix/tasks/aoc.new.ex
defmodule Mix.Tasks.Aoc.New do
  use Mix.Task

  @shortdoc "Generate scaffolding for a new Advent of Code day"
  @moduledoc """
  Creates solution and test files for the given `year` and `day` by copying the
  `y2015/day00` templates and updating the placeholders.

  ## Examples

      mix new 2018 7
  """

  @impl Mix.Task
  def run([year_str, day_str]) do
    with {year, ""} <- Integer.parse(year_str),
         {day, ""} <- Integer.parse(day_str) do
      ensure_bounds!(year, day)

      day_padded = day |> Integer.to_string() |> String.pad_leading(2, "0")

      lib_target = Path.join(["lib", "y#{year}", "day#{day_padded}.ex"])
      test_target = Path.join(["test", "y#{year}", "day#{day_padded}_test.exs"])

      ensure_not_exists!(lib_target)
      ensure_not_exists!(test_target)

      copy_template!("lib/y2015/day00.ex", lib_target, year, day, day_padded)
      copy_template!("test/y2015/day00_test.exs", test_target, year, day, day_padded)

      Mix.shell().info([
        "Generated ",
        lib_target,
        " and ",
        test_target
      ])
    else
      _ ->
        Mix.raise("YEAR and DAY must be integers")
    end
  end

  def run(_args) do
    Mix.raise("Usage: mix new YEAR DAY")
  end

  defp ensure_bounds!(year, day) when year in 1_900..3_000 and day in 0..25, do: :ok

  defp ensure_bounds!(_year, _day) do
    Mix.raise("YEAR must be reasonable and DAY must be between 0 and 25")
  end

  defp ensure_not_exists!(path) do
    if File.exists?(path), do: Mix.raise("#{path} already exists")
  end

  defp copy_template!(source, target, year, day, day_padded) do
    source_content = File.read!(source)
    rendered = render(source_content, year, day, day_padded)

    target |> Path.dirname() |> File.mkdir_p!()
    File.write!(target, rendered)
  end

  defp render(content, year, day, day_padded) do
    content
    |> String.replace("Y2015", "Y#{year}")
    |> String.replace("2015", Integer.to_string(year))
    |> String.replace("Day00", "Day#{day_padded}")
    |> String.replace("day00", "day#{day_padded}")
    |> String.replace("input!(0, #{year})", "input!(#{day}, #{year})")
  end
end

Note that AoC recommends that you don’t use LLMs. Here’s mine:

  1. copy dayN.exs to relevant date
  2. replace REPLACE_ME with relevant date
  3. right click and save input file from website you’re reading already
  4. run with elixir script.exs input

That’s it. That’s the whole setup. :upside_down_face:

Of course - I never use LLMs for solving the problems themselves, just to write the automation around fetching the problems etc.

I have one repo for alllllll of my puzzle solutions that I keep building in every year - GitHub - sevenseacat/advent_of_code: My Elixir solutions to Advent of Code (spoilers: includes solutions for 457 stars and counting)

My daily process is pretty simple -

  • I run mix day <year> <day> eg. mix day 2025 1 to generate a skeleton module/test module for the day’s solution (which will be in lib/y2025/day01.ex and test/y2025/day01_test.exs)
  • I manually download the puzzle input for that day and put it in lib/y2025/input/day01.txt
  • And away I go! Now I can run code like Y2025.Day01.part1() in iex to test out my part 1 solution, etc.
2 Likes

I keep it super-minimal; each day is a subdirectory with part1.exs and part2.exs files (and inputs) and I run code with elixir part1.exs etc

Usually that file defines a module, then has top-level code afterwards that uses the module.

Some of the problems can be solved with literally just a chain of pipes, no module required.

1 Like