Refactor days 7-9 of AoC 2023

- Add `solve` task.
- Add tests for examples.
This commit is contained in:
Ivan R. 2024-12-21 13:40:06 +05:00
parent 669b914523
commit e994381b27
Signed by: lumin
GPG key ID: E0937DC7CD6D3817
40 changed files with 230 additions and 181 deletions

View file

@ -1,4 +1,8 @@
# Day 7: Camel Cards
# Advent of Code 2023 day 7 solution in Elixir
## Camel Cards
[Task page](https://adventofcode.com/2023/day/7)
Your all-expenses-paid trip turns out to be a one-way, five-minute ride in an airship.
(At least it's a cool airship!) It drops you off at the edge of a vast desert and descends back to Island Island.
@ -19,7 +23,8 @@ but the machines have broken down because Desert Island recently stopped receivi
You've already assumed it'll be your job to figure out why the parts stopped when she asks if you can help. You agree automatically.
Because the journey will take a few days, she offers to teach you the game of Camel Cards. Camel Cards is sort of similar to poker except it's designed to be easier to play while riding a camel.
Because the journey will take a few days, she offers to teach you the game of Camel Cards.
Camel Cards is sort of similar to poker except it's designed to be easier to play while riding a camel.
In Camel Cards, you get a list of hands, and your goal is to order them based on the strength of each hand.
A hand consists of five cards labeled one of `A`, `K`, `Q`, `J`, `T`, `9`, `8`, `7`, `6`, `5`, `4`, `3`, or `2`.
@ -27,13 +32,14 @@ The relative strength of each card follows this order, where `A` is the highest
Every hand is exactly one type. From strongest to weakest, they are:
- Five of a kind, where all five cards have the same label: AAAAA
- Four of a kind, where four cards have the same label and one card has a different label: AA8AA
- Full house, where three cards have the same label, and the remaining two cards share a different label: 23332
- Three of a kind, where three cards have the same label, and the remaining two cards are each different from any other card in the hand: TTT98
- Two pair, where two cards share one label, two other cards share a second label, and the remaining card has a third label: 23432
- One pair, where two cards share one label, and the other three cards have a different label from the pair and each other: A23A4
- High card, where all cards' labels are distinct: 23456
- Five of a kind, where all five cards have the same label: `AAAAA`
- Four of a kind, where four cards have the same label and one card has a different label: `AA8AA`
- Full house, where three cards have the same label, and the remaining two cards share a different label: `23332`
- Three of a kind, where three cards have the same label, and the remaining two cards are each different
from any other card in the hand: `TTT98`
- Two pair, where two cards share one label, two other cards share a second label, and the remaining card has a third label: `23432`
- One pair, where two cards share one label, and the other three cards have a different label from the pair and each other: `A23A4`
- High card, where all cards' labels are distinct: `23456`
Hands are primarily ordered based on type; for example, every full house is stronger than any three of a kind.
@ -65,13 +71,16 @@ Because there are five hands in this example, the strongest hand will have rank
So, the first step is to put the hands in order of strength:
- `32T3K` is the only one pair and the other hands are all a stronger type, so it gets rank 1.
- `KK677` and `KTJJT` are both two pair. Their first cards both have the same label, but the second card of `KK677` is stronger (K vs T), so `KTJJT` gets rank 2 and `KK677` gets rank 3.
- `KK677` and `KTJJT` are both two pair. Their first cards both have the same label,
but the second card of `KK677` is stronger (K vs T), so `KTJJT` gets rank 2 and `KK677` gets rank 3.
- `T55J5` and `QQQJA` are both three of a kind. `QQQJA` has a stronger first card, so it gets rank 5 and `T55J5` gets rank 4.
Now, you can determine the total winnings of this set of hands by adding up the result of multiplying each hand's bid with its rank (765 * 1 + 220 * 2 + 28 * 3 + 684 * 4 + 483 * 5). So the total winnings in this example are 6440.
Now, you can determine the total winnings of this set of hands by adding up the result of multiplying each hand's bid with
its rank (765 * 1 + 220 * 2 + 28 * 3 + 684 * 4 + 483 * 5). So the total winnings in this example are 6440.
Find the rank of every hand in your set. What are the total winnings?
## Part Two
To make things a little more interesting, the Elf introduces one additional rule.

View file

@ -1,12 +1,5 @@
defmodule Solution do
@spec get_total_winnings(String.t()) :: integer()
def get_total_winnings(filename, with_jokers \\ false) do
hands =
filename
|> File.stream!()
|> Enum.map(&parse_line/1)
|> Map.new()
defmodule Cards do
def get_total_winnings(hands, with_jokers \\ false) do
sorted_cards =
hands
|> Map.keys()
@ -17,15 +10,6 @@ defmodule Solution do
|> Enum.reduce(0, fn {el, idx}, acc -> acc + idx * hands[el] end)
end
defp parse_line(line) do
[hand, bid] =
line
|> String.trim()
|> String.split(" ")
{hand, String.to_integer(bid)}
end
# Map a hand to a list that may be used for sorting.
defp get_sorting_order(hand, with_jokers) do
# Map cards to corresponding power,
@ -44,13 +28,13 @@ defmodule Solution do
## Examples
iex> Solution.get_card_power("J")
iex> Cards.get_card_power("J")
11
iex> Solution.get_card_power("J", true)
iex> Cards.get_card_power("J", true)
1
iex> Solution.get_card_power("5")
iex> Cards.get_card_power("5")
5
"""
def get_card_power(card, with_jokers \\ false) do
@ -80,25 +64,25 @@ defmodule Solution do
## Examples
iex> Solution.get_rank("AAAAA")
iex> Cards.get_rank("AAAAA")
7
iex> Solution.get_rank("AA8AA")
iex> Cards.get_rank("AA8AA")
6
iex> Solution.get_rank("23332")
iex> Cards.get_rank("23332")
5
iex> Solution.get_rank("TTT98")
iex> Cards.get_rank("TTT98")
4
iex> Solution.get_rank("23432")
iex> Cards.get_rank("23432")
3
iex> Solution.get_rank("A23A4")
iex> Cards.get_rank("A23A4")
2
iex> Solution.get_rank("23456")
iex> Cards.get_rank("23456")
1
"""
def get_rank(hand, with_jokers \\ false) do
@ -134,13 +118,13 @@ defmodule Solution do
## Examples
iex> Solution.replace_joker("T55J5")
iex> Cards.replace_joker("T55J5")
"T5555"
iex> Solution.replace_joker("KTJJT")
iex> Cards.replace_joker("KTJJT")
"KTTTT"
iex> Solution.replace_joker("JJJJJ")
iex> Cards.replace_joker("JJJJJ")
"AAAAA"
"""
def replace_joker(hand) do

View file

@ -0,0 +1,17 @@
defmodule Parser do
def parse_file(filename) do
filename
|> File.stream!()
|> Stream.map(&parse_line/1)
|> Map.new()
end
defp parse_line(line) do
[hand, bid] =
line
|> String.trim()
|> String.split(" ")
{hand, String.to_integer(bid)}
end
end

View file

@ -0,0 +1,10 @@
defmodule Mix.Tasks.Solve do
use Mix.Task
def run(_) do
hands = Parser.parse_file("input.txt")
IO.puts("Part 1: #{Cards.get_total_winnings(hands)}")
IO.puts("Part 2: #{Cards.get_total_winnings(hands, true)}")
end
end

View file

@ -11,18 +11,13 @@ defmodule Day07.MixProject do
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
[]
end
end

View file

@ -0,0 +1,14 @@
defmodule CardsTest do
use ExUnit.Case
doctest Cards
test "example 1" do
hands = Parser.parse_file("test/example.txt")
assert Cards.get_total_winnings(hands) == 6440
end
test "example 2" do
hands = Parser.parse_file("test/example.txt")
assert Cards.get_total_winnings(hands, true) == 5905
end
end

View file

@ -1,4 +1,8 @@
# Day 8
# Advent of Code 2023 day 8 solution in Elixir
## Haunted Wasteland
[Task page](https://adventofcode.com/2023/day/8)
You're still riding a camel across Desert Island when you spot a sandstorm quickly approaching.
When you turn to warn the Elf, she disappears before your eyes! To be fair, she had just finished warning you about ghosts a few minutes ago.
@ -45,6 +49,7 @@ ZZZ = (ZZZ, ZZZ)
Starting at AAA, follow the left/right instructions. How many steps are required to reach ZZZ?
## Part Two
The sandstorm is upon you and you aren't any closer to escaping the wasteland.
@ -79,15 +84,13 @@ Repeat this process until all of the nodes you're currently on end with Z.
(If only some of the nodes you're on end with Z, they act like any other node and you continue as normal.)
In this example, you would proceed as follows:
```
Step 0: You are at 11A and 22A.
Step 1: You choose all of the left paths, leading you to 11B and 22B.
Step 2: You choose all of the right paths, leading you to 11Z and 22C.
Step 3: You choose all of the left paths, leading you to 11B and 22Z.
Step 4: You choose all of the right paths, leading you to 11Z and 22B.
Step 5: You choose all of the left paths, leading you to 11B and 22C.
Step 6: You choose all of the right paths, leading you to 11Z and 22Z.
```
- Step 0: You are at 11A and 22A.
- Step 1: You choose all of the left paths, leading you to 11B and 22B.
- Step 2: You choose all of the right paths, leading you to 11Z and 22C.
- Step 3: You choose all of the left paths, leading you to 11B and 22Z.
- Step 4: You choose all of the right paths, leading you to 11Z and 22B.
- Step 5: You choose all of the left paths, leading you to 11B and 22C.
- Step 6: You choose all of the right paths, leading you to 11Z and 22Z.
So, in this example, you end up entirely on nodes that end in Z after 6 steps.

View file

@ -7,7 +7,6 @@ defmodule Navigator do
iex> Navigator.count_steps(["R"], %{"AAA" => ["AAA", "ZZZ"], "ZZZ" => ["ZZZ", "ZZZ"]})
1
"""
@spec count_steps(list(String.t()), map()) :: integer()
def count_steps(instructions, maps) do
count_steps(instructions, instructions, maps, "AAA", 0)
end

View file

@ -1,16 +1,12 @@
defmodule NavigatorTest do
use ExUnit.Case
doctest Navigator
doctest Parser
defmodule Mix.Tasks.Solve do
use Mix.Task
test "part 1" do
def run(_) do
{instructions, maps} = Parser.parse_file("input.txt")
res = Navigator.count_steps(instructions, maps)
IO.puts("Part 1: #{res}")
end
test "part 2" do
{instructions, maps} = Parser.parse_file("input.txt")
res = Navigator.count_ghost_steps(instructions, maps)
IO.puts("Part 2: #{res}")
end

View file

@ -0,0 +1,25 @@
defmodule Day08.MixProject do
use Mix.Project
def project do
[
app: :day_08,
version: "0.1.0",
elixir: "~> 1.16",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
def application do
[
extra_applications: [:logger]
]
end
defp deps do
[
{:math, "~> 0.7.0"}
]
end
end

View file

@ -0,0 +1,3 @@
%{
"math": {:hex, :math, "0.7.0", "12af548c3892abf939a2e242216c3e7cbfb65b9b2fe0d872d05c6fb609f8127b", [:mix], [], "hexpm", "7987af97a0c6b58ad9db43eb5252a49fc1dfe1f6d98f17da9282e297f594ebc2"},
}

View file

@ -0,0 +1,9 @@
RL
AAA = (BBB, CCC)
BBB = (DDD, EEE)
CCC = (ZZZ, GGG)
DDD = (DDD, DDD)
EEE = (EEE, EEE)
GGG = (GGG, GGG)
ZZZ = (ZZZ, ZZZ)

View file

@ -0,0 +1,10 @@
LR
11A = (11B, XXX)
11B = (XXX, 11Z)
11Z = (11B, XXX)
22A = (22B, XXX)
22B = (22C, 22C)
22C = (22Z, 22Z)
22Z = (22B, 22B)
XXX = (XXX, XXX)

View file

@ -0,0 +1,15 @@
defmodule NavigatorTest do
use ExUnit.Case
doctest Navigator
doctest Parser
test "example 1" do
{instructions, maps} = Parser.parse_file("test/input-1.txt")
assert Navigator.count_steps(instructions, maps) == 2
end
test "example 2" do
{instructions, maps} = Parser.parse_file("test/input-2.txt")
assert Navigator.count_ghost_steps(instructions, maps) == 6
end
end

View file

@ -1,4 +1,8 @@
# Oasis
# Advent of Code 2023 day 9 solution in Elixir
## Mirage Maintenance
[Task page](https://adventofcode.com/2023/day/9)
You ride the camel through the sandstorm and stop where the ghost's maps told you to stop.
The sandstorm subsequently subsides, somehow seeing you standing at an oasis!

View file

@ -1,30 +1,14 @@
defmodule Oasis do
@moduledoc """
Day 9 of Advent of Code 2023.
Link: https://adventofcode.com/2023/day/9.
"""
def solve() do
input =
File.stream!("input.txt")
|> Stream.map(&String.trim_trailing/1)
# Get lists of strings.
|> Stream.map(&String.split/1)
# Get lists of integers.
|> Stream.map(fn list -> Enum.map(list, &String.to_integer/1) end)
# Predict next value for each list.
input
|> Stream.map(fn list -> predict(list, &predict_next_value/2) end)
def get_next_values_sum(lists) do
lists
|> Stream.map(fn list -> Oasis.predict(list, &Oasis.predict_next_value/2) end)
|> Enum.sum()
|> IO.puts()
end
# Predict previous value for each list.
input
|> Stream.map(fn list -> predict(list, &predict_prev_value/2) end)
def get_prev_values_sum(lists) do
lists
|> Stream.map(fn list -> Oasis.predict(list, &Oasis.predict_prev_value/2) end)
|> Enum.sum()
|> IO.puts()
end
@doc """

View file

@ -0,0 +1,8 @@
defmodule Parser do
def parse(filename) do
File.stream!(filename)
|> Stream.map(&String.trim_trailing/1)
|> Stream.map(&String.split/1)
|> Stream.map(fn list -> Enum.map(list, &String.to_integer/1) end)
end
end

View file

@ -0,0 +1,13 @@
defmodule Mix.Tasks.Solve do
use Mix.Task
def run(_) do
input = Parser.parse("input.txt")
res1 = Oasis.get_next_values_sum(input)
IO.puts("Part 1: #{res1}")
res2 = Oasis.get_prev_values_sum(input)
IO.puts("Part 2: #{res2}")
end
end

View file

@ -0,0 +1,23 @@
defmodule Day09.MixProject do
use Mix.Project
def project do
[
app: :day_09,
version: "0.1.0",
elixir: "~> 1.16",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
def application do
[
extra_applications: [:logger]
]
end
defp deps do
[]
end
end

View file

@ -0,0 +1,3 @@
0 3 6 9 12 15
1 3 6 10 15 21
10 13 16 21 30 45

View file

@ -0,0 +1,14 @@
defmodule OasisTest do
use ExUnit.Case
doctest Oasis
test "example 1" do
input = Parser.parse("test/input.txt")
assert Oasis.get_next_values_sum(input) == 114
end
test "example 2" do
input = Parser.parse("test/input.txt")
assert Oasis.get_prev_values_sum(input) == 2
end
end

View file

@ -1,22 +0,0 @@
defmodule SolutionTest do
use ExUnit.Case
doctest Solution
test "example 1" do
assert Solution.get_total_winnings("test/example.txt") == 6440
end
test "part 1" do
res = Solution.get_total_winnings("test/input.txt")
IO.puts("First answer: #{res}")
end
test "example 2" do
assert Solution.get_total_winnings("test/example.txt", true) == 5905
end
test "part 2" do
res = Solution.get_total_winnings("test/input.txt", true)
IO.puts("Second answer: #{res}")
end
end

View file

@ -1,30 +0,0 @@
defmodule Navigator.MixProject do
use Mix.Project
def project do
[
app: :navigator,
version: "0.1.0",
elixir: "~> 1.16",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
{:math, "~> 0.7.0"}
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end

View file

@ -1,5 +0,0 @@
%{
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
"math": {:hex, :math, "0.7.0", "12af548c3892abf939a2e242216c3e7cbfb65b9b2fe0d872d05c6fb609f8127b", [:mix], [], "hexpm", "7987af97a0c6b58ad9db43eb5252a49fc1dfe1f6d98f17da9282e297f594ebc2"},
}

View file

@ -1,28 +0,0 @@
defmodule Oasis.MixProject do
use Mix.Project
def project do
[
app: :oasis,
version: "0.1.0",
elixir: "~> 1.16",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end

View file

@ -1,4 +0,0 @@
defmodule OasisTest do
use ExUnit.Case
doctest Oasis
end