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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
require "logger"

require "letters"
require "minitest"

class Dude
  attr_accessor *%i[ hp damage armor mana ]

  def initialize(hp:, damage: 0, armor: 0, mana: 0)
    @hp, @damage, @armor, @mana = hp, damage, armor, mana
  end
end

class Spell
  NOOP = ->(*){}

  attr_reader *%i[ name cost timer start effect finish ]

  def initialize(name, cost, timer: nil, start: NOOP, effect: NOOP, finish: NOOP)
    @name, @cost, @timer, @start, @effect, @finish = name, cost, timer, start, effect, finish
  end
end

SPELLS = [
  Spell.new(:magic_missile, 53, effect: ->(s) { s.boss.hp -= 4 }),
  Spell.new(:drain, 73, effect: ->(s) { s.boss.hp -= 2; s.player.hp += 2 }),
  Spell.new(:shield, 113, timer: 6,
                          start: ->(s) { s.player.armor += 7 },
                          finish: ->(s) { s.player.armor -= 7 }),
  Spell.new(:poison, 173, timer: 6, effect: ->(s) { s.boss.hp -= 3 }),
  Spell.new(:recharge, 229, timer: 5, effect: ->(s) { s.player.mana += 101 }),
]

class Effect < SimpleDelegator
  attr_accessor *%i[ timer ]

  def initialize(spell)
    super

    @timer = spell.timer
  end
end

class Simulation
  NOOP = ->(_){}

  attr_reader *%i[ boss player log ]
  attr_accessor *%i[ effects ]

  def initialize(boss:, player:, log:)
    @boss, @player, @log = boss, player, log
    @effects = []
  end

  def simulate!(spells)
    spells = spells.dup

    catch(:done) do
      while true
        spell = spells.shift
        throw :done if spell.nil?

        self.player.hp -= 1
        throw :done if self.player.hp <= 0

        self.apply_effects
        self.cast(spell)
        self.check!

        self.apply_effects
        damage = self.boss.damage - self.player.armor
        damage = [damage, 1].max
        self.player.hp -= damage
        self.check!
      end
    end
  end

  def apply_effects
    self.effects.each do |effect|
      effect.effect.call(self)
      effect.timer -= 1
    end

    finished, self.effects = self.effects.partition {|effect| effect.timer.zero? }
    finished.each do |effect|
      effect.finish.call(self)
    end

    self.check!
  end

  def cast(spell)
    throw :done if self.effects.map(&:name).include?(spell.name)

    self.player.mana -= spell.cost
    throw :done if self.player.mana < 0

    spell.start.call(self)

    if spell.timer.nil?
      spell.effect.call(self)
    else
      self.effects << Effect.new(spell)
    end
  end

  def check!
    throw :done if self.player.hp <= 0 ||
                   self.player.mana <= 0 ||
                   self.boss.hp <= 0
  end
end

class TestDay22 < Minitest::Test
  def test_basic
    s = Simulation.new(boss: Dude.new(hp: 13, damage: 8),
                       player: Dude.new(hp: 10, mana: 250))
    s.simulate!(%i[ magic_missile ])

    assert_equal 9, s.boss.hp
    assert_equal 2, s.player.hp
  end

  def test_example_1
    s = Simulation.new(boss: Dude.new(hp: 13, damage: 8),
                       player: Dude.new(hp: 10, mana: 250))
    s.simulate!(%i[ poison magic_missile ])

    assert_equal 0, s.boss.hp
    assert_equal 2, s.player.hp
    assert_equal 24, s.player.mana
  end

  def test_example_2
    s = Simulation.new(boss: Dude.new(hp: 14, damage: 8),
                       player: Dude.new(hp: 10, mana: 250))
    s.simulate!(%i[ recharge shield drain poison magic_missile ])

    assert_equal -1, s.boss.hp
    assert_equal 1, s.player.hp
    assert_equal 114, s.player.mana
  end
end

if __FILE__ == $0
  log = Logger.new(STDOUT)
  log.level = Logger::WARN

  num_spells = 0
  while true
    num_spells += 1
    puts num_spells

    permutations = SPELLS.repeated_permutation(num_spells)
                         .sort_by {|permutation| permutation.map(&:cost).inject(:+) }
    permutations.each do |spells|
      s = Simulation.new(boss: Dude.new(hp: 55, damage: 8),
                         player: Dude.new(hp: 50, mana: 500),
                         log: log)
      s.simulate!(spells)

      if s.player.hp > 0 && s.player.mana > 0 && s.boss.hp <= 0
        p spells.map(&:name)
        puts spells.map(&:cost).inject(:+)
        exit
      end
    end
  end
end