Advent of code 2015 Day 21 Part 1 & Part 2 を Livebook で楽しむ

## はじめに

Advent of code 2024 の準備として、過去回の Advent of code 2015 を Livebook で楽しみます

本記事では Day 21 の Part 1 と Part 2 を解きます

問題文はこちら

https://adventofcode.com/2015/day/21

実装したノートブックはこちら

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/aoc/2015/2015_day21_part1.livemd

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/aoc/2015/2015_day21_part2.livemd

## セットアップ

Kino AOC をインストールします

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

Kino AOC の使い方はこちらを参照

https://qiita.com/RyoWakabayashi/items/3d3abcf9134cee4dff05

## 入力の取得

"Advent of Code Helper" スマートセルを追加し、 Day 21 の入力を取得します

![スクリーンショット 2024-12-04 19.58.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/f9a9114d-13cc-ed1b-4274-79b12464b70e.png)

## 私の答え

私の答えです。
折りたたんでおきます。
▶を押して開いてください。

### Part 1

<details><summary>回答</summary>

まずはお店のメニューを読み込みます

```elixir
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
```

```elixir
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}
```

実行結果

```elixir
{[
   %{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}
 ]}
```

決着が着くまで再帰的に戦うモジュールを用意します

```elixir
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
```

入力例で戦ってみます

```elixir
player = %{
  hp: 8,
  damage: 5,
  armor: 5
}

boss = %{
  hp: 12,
  damage: 7,
  armor: 2
}

Game.battle(player, boss)
```

実行結果

```elixir
:win
```

プレイヤーが勝利しました

全ての装備の組み合わせを作ります

まず、配列から n 個の要素を取り出す組み合わせを全て作るモジュールを用意します

```elixir
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 個の範囲で装備できます

各個数についてそれぞれ全ての組み合わせを作り、さらに武器と鎧と指輪の組み合わせを全て組み合わせます

このとき、コスト、攻撃力、守備力の合計を装備の値として計算しておきます

```elixir
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
```

実行結果

```elixir
[
  %{cost: 8, damage: 4, armor: 0},
  %{cost: 33, damage: 5, armor: 0},
  %{cost: 58, damage: 6, armor: 0},
  ...
]
```

ボスのパラメーターを入力から読み込みます

```elixir
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)
```

実行結果

```elixir
%{damage: 9, armor: 2, hp: 103}
```

装備の組み合わせをコストの低い順に並べ、ボスに勝てる組み合わせを探索します

```elixir
equipment_combinations
|> Enum.sort_by(&(&1.cost))
|> Enum.find(fn equipments ->
  Game.battle(Map.put(equipments, :hp, 100), boss) == :win
end)
```

</details>

### Part 2

<details><summary>回答</summary>

Part 1 の逆です

装備をコストの高い順に並べ、ボスに負ける組み合わせを探索します

```elixir
equipment_combinations
|> Enum.sort_by(&(&1.cost), :desc)
|> Enum.find(fn equipments ->
  Game.battle(Map.put(equipments, :hp, 100), boss) == :lose
end)
```

</details>

## まとめ

問題文から ChatGPT に画像を生成してもらいました

![aoc2015_day21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/1c338cb0-6805-a0e2-1207-49bbd9ec9105.png)

RPG を作っているようでワクワクする問題ですね