diff --git a/2018/ruby/Gemfile b/2018/ruby/Gemfile
index e0b6838..88bd7be 100644
--- a/2018/ruby/Gemfile
+++ b/2018/ruby/Gemfile
@@ -4,4 +4,5 @@ source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
+gem "minitest"
gem "pry"
diff --git a/2018/ruby/Gemfile.lock b/2018/ruby/Gemfile.lock
index 05484ac..78d0fa4 100644
--- a/2018/ruby/Gemfile.lock
+++ b/2018/ruby/Gemfile.lock
@@ -3,6 +3,7 @@ GEM
specs:
coderay (1.1.2)
method_source (0.9.2)
+ minitest (5.11.3)
pry (0.12.2)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
@@ -11,6 +12,7 @@ PLATFORMS
ruby
DEPENDENCIES
+ minitest
pry
BUNDLED WITH
diff --git a/2018/ruby/day_15.rb b/2018/ruby/day_15.rb
index 9430e00..6fe23e6 100644
--- a/2018/ruby/day_15.rb
+++ b/2018/ruby/day_15.rb
@@ -1,10 +1,11 @@
require "set"
require "minitest"
+require "minitest/pride"
-class Combat
- class EndCombat < StandardError; end
+class EndCombat < StandardError; end
+class Combat
def initialize(map)
@map = map
end
@@ -12,12 +13,32 @@ class Combat
def fight
return enum_for(__method__) unless block_given?
- turn_order.each do |pos|
- end
-
yield @map
+
+ loop do
+ turn_order.each do |pos|
+ next if @map[pos].nil? # don't take a turn for units that just died
+
+ turn = Turn.new(@map, pos)
+
+ if move = turn.move
+ unit = @map.delete(pos)
+ @map[move] = unit
+ turn.pos = move
+ end
+
+ if attack = turn.attack
+ unit = @map.fetch(turn.pos)
+ target = @map.fetch(attack)
+ target.hp -= unit.attack
+ @map.delete(attack) if target.hp < 0
+ end
+ end
+
+ yield @map
+ end
rescue EndCombat
- yield @map
+ # yield @map
end
def turn_order
@@ -26,7 +47,7 @@ class Combat
end
class TestCombat < Minitest::Test
- def test_fight
+ def test_move
map = Map.parse(<<~MAP)
#########
#G..G..G#
@@ -40,9 +61,9 @@ class TestCombat < Minitest::Test
MAP
combat = Combat.new(map)
rounds = combat.fight
+ rounds.next # skip the first round
- map = rounds.next
- assert_equal <<~MAP.chomp, map.to_s
+ assert_equal <<~MAP.chomp, rounds.next.to_s
#########
#.G...G.#
#...G...#
@@ -53,6 +74,60 @@ class TestCombat < Minitest::Test
#.......#
#########
MAP
+
+ assert_equal <<~MAP.chomp, rounds.next.to_s
+ #########
+ #..G.G..#
+ #...G...#
+ #.G.E.G.#
+ #.......#
+ #G..G..G#
+ #.......#
+ #.......#
+ #########
+ MAP
+
+ assert_equal <<~MAP.chomp, rounds.next.to_s
+ #########
+ #.......#
+ #..GGG..#
+ #..GEG..#
+ #G..G...#
+ #......G#
+ #.......#
+ #.......#
+ #########
+ MAP
+ end
+
+ def test_fight
+ map = Map.parse(<<~MAP)
+ #######
+ #.G...#
+ #...EG#
+ #.#.#G#
+ #..G#E#
+ #.....#
+ #######
+ MAP
+ combat = Combat.new(map)
+ rounds = combat.fight
+ rounds.next # skip the initial state
+ rounds.next # skip the first found
+
+ map = rounds.next # second round
+ assert_equal 188, map.fetch(Position[2,4]).hp
+
+ 20.times { rounds.next }
+ assert_equal <<~MAP.chomp, rounds.next.to_s
+ #######
+ #...G.#
+ #..G.G#
+ #.#.#G#
+ #...#E#
+ #.....#
+ #######
+ MAP
end
def test_turn_order
@@ -71,6 +146,8 @@ class TestCombat < Minitest::Test
end
class Turn
+ attr_accessor :pos
+
def initialize(map, pos)
@map, @pos = map, pos
@unit = @map.fetch(@pos)
@@ -79,6 +156,7 @@ class Turn
def move
raise EndCombat if targets.empty?
return if can_attack?
+ return if nearest.empty?
chosen = nearest.min
haystack = @map.in_range(@pos)
@@ -88,12 +166,20 @@ class Turn
.first
end
+ def attack
+ attackable.min_by {|pos, target| [target.hp, pos] }&.first
+ end
+
def targets
@map.units.select {|_, other| other.is_a?(@unit.enemy) }
end
+ def attackable
+ targets.select {|pos, _| @pos.adjacent.include?(pos) }
+ end
+
def can_attack?
- @map.in_range(@pos).any? {|pos| targets.has_key?(pos) }
+ !attackable.empty?
end
def in_range
@@ -109,6 +195,8 @@ class Turn
reachable.each do |pos, distance|
nearest[distance] << pos
end
+
+ return [] if nearest.empty?
nearest.min_by(&:first).last
end
@@ -199,18 +287,25 @@ class Map
@occupied[pos]
end
+ def []=(pos, square)
+ @occupied[pos] = square
+ end
+
def fetch(pos)
@occupied.fetch(pos)
end
+ def delete(pos)
+ @occupied.delete(pos)
+ end
+
def units
@occupied.select {|_, sq| sq.is_a?(Unit) }
end
def in_range(pos)
- [-1, 1]
- .flat_map {|d| [[pos.y, pos.x+d], [pos.y+d, pos.x]] }
- .map {|pos| Position[*pos] }
+ pos
+ .adjacent
.reject {|pos| @occupied.has_key?(pos) }
end
@@ -281,12 +376,33 @@ Position = Struct.new(:y, :x) do
[self.y, self.x] <=> [other.y, other.x]
end
+ def adjacent
+ [-1, 1]
+ .flat_map {|d| [[y, x+d], [y+d, x]] }
+ .map {|pos| Position[*pos] }
+ end
+
def to_a
[y, x]
end
+
+ def to_s
+ to_a.to_s
+ end
end
class Unit
+ attr_reader :attack
+ attr_accessor :hp
+
+ def initialize
+ @attack = 3
+ @hp = 200
+ end
+
+ def to_s
+ "#{self.class.name.chars.first}(#{hp})"
+ end
end
class Wall
@@ -304,8 +420,85 @@ class Goblin < Unit
end
end
+def solve(input)
+ map = Map.parse(input)
+ combat = Combat.new(map)
+ map, count = combat.fight.map.with_index
+ .inject(nil) {|last,(map,count)|
+ # puts map, map.units.values.map(&:to_s).inspect
+ [map, count]
+ }
+ outcome = map.units.values.map(&:hp).sum * count
+end
+
+class TestSolve < Minitest::Test
+ def test_solve
+ assert_equal 27730, solve(<<~INPUT)
+ #######
+ #.G...#
+ #...EG#
+ #.#.#G#
+ #..G#E#
+ #.....#
+ #######
+ INPUT
+
+ assert_equal 36334, solve(<<~INPUT)
+ #######
+ #G..#E#
+ #E#E.E#
+ #G.##.#
+ #...#E#
+ #...E.#
+ #######
+ INPUT
+
+ assert_equal 39514, solve(<<~INPUT)
+ #######
+ #E..EG#
+ #.#G.E#
+ #E.##E#
+ #G..#.#
+ #..E#.#
+ #######
+ INPUT
+
+ assert_equal 27755, solve(<<~INPUT)
+ #######
+ #E.G#.#
+ #.#G..#
+ #G.#.G#
+ #G..#.#
+ #...E.#
+ #######
+ INPUT
+
+ assert_equal 28944, solve(<<~INPUT)
+ #######
+ #.E...#
+ #.#..G#
+ #.###.#
+ #E#G#G#
+ #...#G#
+ #######
+ INPUT
+
+ assert_equal 18740, solve(<<~INPUT)
+ #########
+ #G......#
+ #.E.#...#
+ #..##..G#
+ #...##..#
+ #...#...#
+ #.G...G.#
+ #.....G.#
+ #########
+ INPUT
+ end
+end
+
if __FILE__ == $0
require "minitest/autorun" and exit if ENV["TEST"]
-
+ puts solve(ARGF.read)
end