はじめに
Advent of code 2024 の準備として、過去回の Advent of code 2015 を Livebook で楽しみます
本記事では Day 21 の Part 1 と Part 2 を解きます
問題文はこちら
実装したノートブックはこちら
セットアップ
Kino AOC をインストールします
Mix.install([
{:kino_aoc, "~> 0.1"}
])
Kino AOC の使い方はこちらを参照
入力の取得
"Advent of Code Helper" スマートセルを追加し、 Day 21 の入力を取得します
私の答え
私の答えです。
折りたたんでおきます。
▶を押して開いてください。
Part 1
回答
まずはお店のメニューを読み込みます
load_shop_menu = fn menu ->
menu
|> String.trim()
|> String.split("\n")
|> Enum.map(fn row ->
row
|> String.split(" ", trim: true)
|> then(fn [name, cost, damage, armor] ->
%{
name: name,
cost: cost |> String.trim() |> String.to_integer(),
damage: damage |> String.trim() |> String.to_integer(),
armor: armor |> String.trim() |> String.to_integer(),
}
end)
end)
end
weapons =
"""
Dagger 8 4 0
Shortsword 10 5 0
Warhammer 25 6 0
Longsword 40 7 0
Greataxe 74 8 0
"""
|> load_shop_menu.()
armors =
"""
Leather 13 0 1
Chainmail 31 0 2
Splintmail 53 0 3
Bandedmail 75 0 4
Platemail 102 0 5
"""
|> load_shop_menu.()
rings =
"""
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
"""
|> load_shop_menu.()
{weapons, armors, rings}
実行結果
{[
%{name: "Dagger", cost: 8, damage: 4, armor: 0},
%{name: "Shortsword", cost: 10, damage: 5, armor: 0},
%{name: "Warhammer", cost: 25, damage: 6, armor: 0},
%{name: "Longsword", cost: 40, damage: 7, armor: 0},
%{name: "Greataxe", cost: 74, damage: 8, armor: 0}
],
[
%{name: "Leather", cost: 13, damage: 0, armor: 1},
%{name: "Chainmail", cost: 31, damage: 0, armor: 2},
%{name: "Splintmail", cost: 53, damage: 0, armor: 3},
%{name: "Bandedmail", cost: 75, damage: 0, armor: 4},
%{name: "Platemail", cost: 102, damage: 0, armor: 5}
],
[
%{name: "Damage +1", cost: 25, damage: 1, armor: 0},
%{name: "Damage +2", cost: 50, damage: 2, armor: 0},
%{name: "Damage +3", cost: 100, damage: 3, armor: 0},
%{name: "Defense +1", cost: 20, damage: 0, armor: 1},
%{name: "Defense +2", cost: 40, damage: 0, armor: 2},
%{name: "Defense +3", cost: 80, damage: 0, armor: 3}
]}
決着が着くまで再帰的に戦うモジュールを用意します
defmodule Game do
def battle(player, boss) do
boss_hp = boss.hp - max(1, player.damage - boss.armor)
if boss_hp < 1 do
:win
else
player_hp = player.hp - max(1, boss.damage - player.armor)
if player_hp < 1 do
:lose
else
battle(
Map.put(player, :hp, player_hp),
Map.put(boss, :hp, boss_hp)
)
end
end
end
end
入力例で戦ってみます
player = %{
hp: 8,
damage: 5,
armor: 5
}
boss = %{
hp: 12,
damage: 7,
armor: 2
}
Game.battle(player, boss)
実行結果
:win
プレイヤーが勝利しました
全ての装備の組み合わせを作ります
まず、配列から n 個の要素を取り出す組み合わせを全て作るモジュールを用意します
defmodule Combinations do
def all(_, 0), do: [[]]
def all([], _), do: []
def all(list, n) when length(list) == n, do: [list]
def all([head | tail], n) do
with_head = for combo <- all(tail, n - 1), do: [head | combo]
without_head = all(tail, n)
with_head ++ without_head
end
end
武器は必ず 1 個、鎧は 0 か 1 個、指輪は 0 から 2 個の範囲で装備できます
各個数についてそれぞれ全ての組み合わせを作り、さらに武器と鎧と指輪の組み合わせを全て組み合わせます
このとき、コスト、攻撃力、守備力の合計を装備の値として計算しておきます
weapon_combinations = Combinations.all(weapons, 1)
armor_combinations = Combinations.all(armors, 0) ++ Combinations.all(armors, 1)
ring_combinations =
Combinations.all(rings, 0) ++ Combinations.all(rings, 1) ++ Combinations.all(rings, 2)
equipment_combinations =
for weapon <- weapon_combinations,
armor <- armor_combinations,
ring <- ring_combinations do
(weapon ++ armor ++ ring)
|> Enum.reduce(%{cost: 0, damage: 0, armor: 0}, fn equipment, acc_equipments ->
acc_equipments
|> Map.put(:cost, acc_equipments.cost + equipment.cost)
|> Map.put(:damage, acc_equipments.damage + equipment.damage)
|> Map.put(:armor, acc_equipments.armor + equipment.armor)
end)
end
実行結果
[
%{cost: 8, damage: 4, armor: 0},
%{cost: 33, damage: 5, armor: 0},
%{cost: 58, damage: 6, armor: 0},
...
]
ボスのパラメーターを入力から読み込みます
boss =
puzzle_input
|> String.split("\n")
|> Enum.map(fn row ->
row
|> String.split(": ")
|> Enum.at(1)
|> String.to_integer()
end)
|> then(fn [hp, damage, armor] ->
%{hp: hp, damage: damage, armor: armor}
end)
実行結果
%{damage: 9, armor: 2, hp: 103}
装備の組み合わせをコストの低い順に並べ、ボスに勝てる組み合わせを探索します
equipment_combinations
|> Enum.sort_by(&(&1.cost))
|> Enum.find(fn equipments ->
Game.battle(Map.put(equipments, :hp, 100), boss) == :win
end)
Part 2
回答
Part 1 の逆です
装備をコストの高い順に並べ、ボスに負ける組み合わせを探索します
equipment_combinations
|> Enum.sort_by(&(&1.cost), :desc)
|> Enum.find(fn equipments ->
Game.battle(Map.put(equipments, :hp, 100), boss) == :lose
end)
まとめ
問題文から ChatGPT に画像を生成してもらいました
RPG を作っているようでワクワクする問題ですね