Day 22.0
change
commit 939a723ec26924d15031e000b8d590e019833394
author Alpha Chen <alpha.chen@gmail.com>
date
parent d2f5cf0c
diff --git a/ruby/day_22.rb b/ruby/day_22.rb
new file mode 100644
index 0000000..aca6dd0
--- /dev/null
+++ b/ruby/day_22.rb
@@ -0,0 +1,167 @@
+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.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