はじめに
Advent of code 2024 の準備として、過去回の Advent of code 2015 を Livebook で楽しみます
本記事では Day 22 の Part 1 と Part 2 を解きます
問題文はこちら
実装したノートブックはこちら
セットアップ
Kino AOC をインストールします
Mix.install([
{:kino_aoc, "~> 0.1"}
])
Kino AOC の使い方はこちらを参照
入力の取得
"Advent of Code Helper" スマートセルを追加し、 Day 22 の入力を取得します
私の答え
私の答えです。
折りたたんでおきます。
▶を押して開いてください。
Part 1
回答
自力だけだと解けなかったので、とうとう ChatGPT の助けを借りてしまいました
ルールに則った戦闘をしながら、消費したマナを記録していき、最小値を求めます
次のターンで詠唱可能な魔法を選択しながら再帰するのが肝ですね
defmodule Spell do
defstruct name: nil, cost: 0, damage: 0, heal: 0, effect: nil
end
defmodule Effect do
defstruct name: nil, timer: 0, armor: 0, damage: 0, mana: 0
end
defmodule WizardSimulator do
@spells [
%Spell{name: :magic_missile, cost: 53, damage: 4},
%Spell{name: :drain, cost: 73, damage: 2, heal: 2},
%Spell{name: :shield, cost: 113, effect: %Effect{name: :shield, timer: 6, armor: 7}},
%Spell{name: :poison, cost: 173, effect: %Effect{name: :poison, timer: 6, damage: 3}},
%Spell{name: :recharge, cost: 229, effect: %Effect{name: :recharge, timer: 5, mana: 101}}
]
def find_min_mana_to_win(player_hp, player_mana, boss_hp, boss_damage) do
initial_state = %{
player_hp: player_hp,
player_mana: player_mana,
player_armor: 0,
boss_hp: boss_hp,
boss_damage: boss_damage,
effects: %{},
mana_spent: 0,
turn: :player
}
play_game([initial_state], :infinity)
end
defp play_game([], min_mana_spent), do: min_mana_spent
defp play_game([state | rest], min_mana_spent) do
state = apply_effects(state)
cond do
state.boss_hp <= 0 ->
min_mana_spent = min(state.mana_spent, min_mana_spent)
play_game(rest, min_mana_spent)
state.player_hp <= 0 ->
play_game(rest, min_mana_spent)
state.mana_spent >= min_mana_spent ->
play_game(rest, min_mana_spent)
state.turn == :player ->
available_spells = available_spells(state)
case available_spells do
[] ->
play_game(rest, min_mana_spent)
available_spells ->
available_spells
|> Enum.map(fn spell ->
cast_spell(state, spell)
end)
|> Kernel.++(rest)
|> play_game(min_mana_spent)
end
state.turn == :boss ->
state
|> boss_attack()
|> Map.put(:turn, :player)
|> then(fn state -> [state | rest] end)
|> play_game(min_mana_spent)
end
end
defp check_effects_timer(effects, effect, magic_name) do
effect = %{effect | timer: effect.timer - 1}
if effect.timer == 0 do
Map.delete(effects, magic_name)
else
Map.put(effects, magic_name, effect)
end
end
defp apply_effects(state) do
{effects, player_armor, player_mana, boss_hp} =
Enum.reduce(state.effects, {state.effects, 0, state.player_mana, state.boss_hp}, fn
{:shield, effect}, {effects, armor, mana, hp} ->
armor = armor + effect.armor
effects = check_effects_timer(effects, effect, :shield)
{effects, armor, mana, hp}
{:poison, effect}, {effects, armor, mana, hp} ->
hp = hp - effect.damage
effects = check_effects_timer(effects, effect, :poison)
{effects, armor, mana, hp}
{:recharge, effect}, {effects, armor, mana, hp} ->
mana = mana + effect.mana
effects = check_effects_timer(effects, effect, :recharge)
{effects, armor, mana, hp}
end)
%{
state
| effects: effects,
player_armor: player_armor,
player_mana: player_mana,
boss_hp: boss_hp
}
end
defp available_spells(state) do
@spells
|> Enum.filter(fn spell ->
spell.cost <= state.player_mana and
not Map.has_key?(state.effects, spell.effect && spell.effect.name)
end)
end
defp cast_spell(state, spell) do
state =
%{
state
| player_mana: state.player_mana - spell.cost,
mana_spent: state.mana_spent + spell.cost
}
state =
if spell.damage > 0 or spell.heal > 0 do
%{
state
| boss_hp: state.boss_hp - spell.damage,
player_hp: state.player_hp + spell.heal
}
else
state
end
if spell.effect do
%{state | effects: Map.put(state.effects, spell.effect.name, spell.effect)}
else
state
end
|> Map.put(:turn, :boss)
end
defp boss_attack(state) do
%{state | player_hp: state.player_hp - max(1, state.boss_damage - state.player_armor)}
end
end
入力からボスのパラメーターを取得します
[boss_hp, boss_damage] =
Regex.scan(
~r/\d+/,
puzzle_input
)
|> Enum.map(fn [str] ->
String.to_integer(str)
end)
{boss_hp, boss_damage}
実行結果
{58, 9}
プレイヤーのパラメーターとボスのパラメーターを使って答えを出します
WizardSimulator.find_min_mana_to_win(50, 500, boss_hp, boss_damage)
Part 2
回答
ハードモードとして、プレイヤーのターンに毎回 1 ダメージを喰らうようになります
Part 1 のモジュールで、 play_game
関数の冒頭にダメージ判定を加えるだけです
defmodule WizardSimulator do
...
defp play_game([state | rest], min_mana_spent) do
state =
if state.turn == :player do
%{state | player_hp: state.player_hp - 1}
else
state
end
|> apply_effects()
...
end
まとめ
問題文から ChatGPT に画像を生成してもらいました
2日連続 RPG です
途中までは自力で解いたのですが、ややこしすぎて ChatGPT を頼ったら一発で解いてくれました
もはや AI には敵わないですね