From df0c1df525ab07ead9d49f32297cc04ed810de96 Mon Sep 17 00:00:00 2001 From: Alpha Chen Date: Mon, 24 Dec 2018 16:56:36 -0700 Subject: [PATCH] [2018][ruby][15.1] movement --- 2018/ruby/day_15.rb | 267 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 242 insertions(+), 25 deletions(-) diff --git a/2018/ruby/day_15.rb b/2018/ruby/day_15.rb index 154d324..9430e00 100644 --- a/2018/ruby/day_15.rb +++ b/2018/ruby/day_15.rb @@ -1,23 +1,190 @@ +require "set" + require "minitest" class Combat + class EndCombat < StandardError; end + + def initialize(map) + @map = map + end + + def fight + return enum_for(__method__) unless block_given? + + turn_order.each do |pos| + end + + yield @map + rescue EndCombat + yield @map + end + + def turn_order + @map.units.keys.sort + end +end + +class TestCombat < Minitest::Test + def test_fight + map = Map.parse(<<~MAP) + ######### + #G..G..G# + #.......# + #.......# + #G..E..G# + #.......# + #.......# + #G..G..G# + ######### + MAP + combat = Combat.new(map) + rounds = combat.fight + + map = rounds.next + assert_equal <<~MAP.chomp, map.to_s + ######### + #.G...G.# + #...G...# + #...E..G# + #.G.....# + #.......# + #G..G..G# + #.......# + ######### + MAP + end + + def test_turn_order + map = Map.parse(<<~MAP) + ####### + #.G.E.# + #E.G.E# + #.G.E.# + ####### + MAP + combat = Combat.new(map) + + expected = [[1,2], [1,4], [2,1], [2,3], [2,5], [3,2], [3,4]].map {|pos| Position[*pos]} + assert_equal expected, combat.turn_order + end +end + +class Turn + def initialize(map, pos) + @map, @pos = map, pos + @unit = @map.fetch(@pos) + end + + def move + raise EndCombat if targets.empty? + return if can_attack? + + chosen = nearest.min + haystack = @map.in_range(@pos) + dijkstra(chosen) + .select {|pos,_| haystack.include?(pos) } + .min_by {|pos,distance| [distance, pos] } + .first + end + + def targets + @map.units.select {|_, other| other.is_a?(@unit.enemy) } + end + + def can_attack? + @map.in_range(@pos).any? {|pos| targets.has_key?(pos) } + end + + def in_range + targets.flat_map {|pos, _| @map.in_range(pos) }.uniq + end + + def reachable + dijkstra(@pos).select {|pos,_| in_range.include?(pos) } + end + + def nearest + nearest = Hash.new {|h,k| h[k] = [] } + reachable.each do |pos, distance| + nearest[distance] << pos + end + nearest.min_by(&:first).last + end + + private + + def dijkstra(pos) + distances = { pos => 0 } + queue = [ pos ] + while pos = queue.shift + distance = distances.fetch(pos) + 1 + @map.in_range(pos).reject {|pos| distances.has_key?(pos) }.each do |pos| + distances[pos] = distance + queue << pos + end + end + distances + end +end + +class TestTurn < Minitest::Test + def setup + map = Map.parse(<<~MAP) + ####### + #E..G.# + #...#.# + #.G.#G# + ####### + MAP + @turn = Turn.new(map, Position[1,1]) + end + + def test_targets + assert_equal [[1,4], [3,2], [3,5]].to_set, @turn.targets.keys.map(&:to_a).to_set + end + + def test_in_range + assert_equal [[1,3], [1,5], [2,2], [2,5], [3,1], [3,3]].to_set, @turn.in_range.map(&:to_a).to_set + end + + def test_reachable + assert_equal [[1,3], [2,2], [3,1], [3,3]].to_set, @turn.reachable.keys.map(&:to_a).to_set + end + + def test_nearest + assert_equal [[1,3], [2,2], [3,1]].to_set, @turn.nearest.map(&:to_a).to_set + end + + def test_move + assert_equal Position[1,2], @turn.move + + turn = Turn.new(Map.parse(<<~MAP), Position[1,2]) + ####### + #.E...# + #.....# + #...G.# + ####### + MAP + assert_equal Position[1,3], turn.move + end end -class Grid +class Map def self.parse(s) occupied = {} s.chomp.lines.each.with_index do |row, y| - row.chomp.chars.each.with_index do |pos, x| - case pos + row.chomp.chars.each.with_index do |square, x| + case square when ?# - occupied[[y, x]] = :wall + occupied[Position[y, x]] = Wall.new when ?E - occupied[[y, x]] = Elf.new + occupied[Position[y, x]] = Elf.new when ?G - occupied[[y, x]] = Goblin.new + occupied[Position[y, x]] = Goblin.new when ?. else - fail "Invalid character: #{pos.inspect}" + fail "Invalid character: #{square.inspect}" end end end @@ -29,17 +196,31 @@ class Grid end def [](pos) - y, x = pos - @occupied[[y, x]] + @occupied[pos] + end + + def fetch(pos) + @occupied.fetch(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] } + .reject {|pos| @occupied.has_key?(pos) } end def to_s - max_y = @occupied.keys.map(&:first).max - max_x = @occupied.keys.map(&:last).max + max_y = @occupied.keys.map(&:y).max + max_x = @occupied.keys.map(&:x).max (0..max_y).map {|y| (0..max_x).map {|x| - case @occupied[[y, x]] - when :wall + case @occupied[Position[y, x]] + when Wall ?# when Elf ?E @@ -48,43 +229,79 @@ class Grid when nil ?. else - fail "Unexpected object: #{@occupied[[y, x]]}" + fail "Unexpected object: #{@occupied[Position[y, x]]}" end }.join }.join(?\n) end end -class TestGrid < Minitest::Test +class TestMap < Minitest::Test def test_parse - grid = Grid.parse(<<~GRID) + map = Map.parse(<<~MAP) + ####### + #E..G.# + #...#.# + #.G.#G# + ####### + MAP + + assert_instance_of Wall, map[Position[0,0]] + assert_instance_of Wall, map[Position[2,4]] + assert_instance_of Wall, map[Position[4,6]] + + assert_instance_of Elf, map[Position[1,1]] + + assert_instance_of Goblin, map[Position[1,4]] + assert_instance_of Goblin, map[Position[3,5]] + + assert_nil map[Position[1,2]] + assert_nil map[Position[2,5]] + end + + def test_units + map = Map.parse(<<~MAP) ####### #E..G.# #...#.# #.G.#G# ####### - GRID + MAP - assert_equal :wall, grid[[0,0]] - assert_equal :wall, grid[[2,4]] - assert_equal :wall, grid[[4,6]] + assert_equal 4, map.units.length + end +end - assert_instance_of Elf, grid[[1,1]] - assert_instance_of Goblin, grid[[1,4]] - assert_instance_of Goblin, grid[[3,5]] +Position = Struct.new(:y, :x) do + def self.[](y, x) + self.new(y, x) + end - assert_nil grid[[1,2]] - assert_nil grid[[2,5]] + def <=>(other) + [self.y, self.x] <=> [other.y, other.x] + end + + def to_a + [y, x] end end class Unit end +class Wall +end + class Elf < Unit + def enemy + Goblin + end end class Goblin < Unit + def enemy + Elf + end end if __FILE__ == $0