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