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