6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ElixirAdvent Calendar 2024

Day 16

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

Last updated at Posted at 2024-12-05

はじめに

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 の入力を取得します

スクリーンショット 2024-12-04 20.13.09.png

私の答え

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

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 に画像を生成してもらいました

aoc2015_day22.png

2日連続 RPG です

途中までは自力で解いたのですが、ややこしすぎて ChatGPT を頼ったら一発で解いてくれました

もはや AI には敵わないですね

6
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?