-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathday_21.ex
131 lines (112 loc) · 3.52 KB
/
day_21.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
defmodule AdventOfCode.Y2015.Day21 do
@moduledoc """
--- Day 21: RPG Simulator 20XX ---
Problem Link: https://adventofcode.com/2015/day/21
Difficulty: l
Tags: double-parse data-modelling combinatorics strategy
"""
alias AdventOfCode.Algorithms.Combinatorics
alias AdventOfCode.Helpers.{InputReader, Transformers}
@item_db ~S"""
Weapons: Cost Damage Armor
Dagger 8 4 0
Shortsword 10 5 0
Warhammer 25 6 0
Longsword 40 7 0
Greataxe 74 8 0
Armor: Cost Damage Armor
Leather 13 0 1
Chainmail 31 0 2
Splintmail 53 0 3
Bandedmail 75 0 4
Platemail 102 0 5
Rings: Cost Damage Armor
Damage +1 25 1 0
Damage +2 50 2 0
Damage +3 100 3 0
Defense +1 20 0 1
Defense +2 40 0 2
Defense +3 80 0 3
"""
def input, do: InputReader.read_from_file(2015, 21)
def run(input \\ input()) do
input = parse(input)
{frugal_win(input), expensive_loss(input)}
end
defp frugal_win(boss) do
find_determining_combination(store(), boss, &Enum.filter/2, &Enum.min_by/2)
end
defp expensive_loss(boss) do
find_determining_combination(store(), boss, &Enum.reject/2, &Enum.max_by/2)
end
defp find_determining_combination(store, boss, filter, max_or_min) do
store
|> possible_stats()
|> filter.(fn stat ->
stat
|> Map.merge(%{hit_points: 100})
|> player_wins?(boss)
end)
|> max_or_min.(& &1.cost)
|> then(& &1.cost)
end
def parse(data \\ input()) do
data
|> Transformers.lines()
|> Map.new(fn line ->
[attr, pts] = String.split(line, ": ")
{to_attr(attr), String.to_integer(pts)}
end)
end
defp player_wins?(player, boss) do
number_of_rounds(boss, player) <= number_of_rounds(player, boss)
end
defp number_of_rounds(defender, attacker) do
score_deduction = max(attacker.damage - defender.armor, 1)
div(defender.hit_points, score_deduction)
end
defp to_attr("Hit Points"), do: :hit_points
defp to_attr("Damage"), do: :damage
defp to_attr("Armor"), do: :armor
defp to_attr("Rings"), do: :rings
defp to_attr("Weapons"), do: :weapons
def store do
@item_db
|> String.split(~r{(\r\n\r\n|\r\r|\n\n)}, trim: true)
|> Enum.map(&Transformers.lines/1)
|> Map.new(fn [title | data] ->
{
String.split(title, ": ") |> List.first() |> to_attr(),
data
|> Enum.map(fn line ->
line
|> Transformers.words()
|> Enum.slice(-3, 3)
|> parse_store_item()
end)
}
end)
end
defp parse_store_item(values) do
[cost, damage, armor] = Enum.map(values, &String.to_integer/1)
%{cost: cost, damage: damage, armor: armor}
end
def possible_stats(store) do
for weapons <- weapons_combinations(store),
armor <- armor_combinations(store),
rings <- rings_combinations(store) do
[weapons, armor, rings]
|> Enum.concat()
|> Enum.reduce(&Map.merge(&1, &2, fn _, a, b -> a + b end))
end
end
defp weapons_combinations(%{weapons: weapons}), do: combinations(weapons, 1, 1)
defp armor_combinations(%{armor: armor}), do: combinations(armor, 0, 1)
defp rings_combinations(%{rings: rings}), do: combinations(rings, 0, 2)
defp combinations(item_store, lower, upper) do
lower..upper
|> Enum.flat_map(fn qty ->
Combinatorics.combinations(item_store, qty)
end)
end
end