[2018][ruby][15.1] movement
change tqsozryosxzwwlqvvnkqxoqouslzmupv
commit f211e24e0770488069ad9e68556b92d119c1ba33
author Alpha Chen <alpha.chen@gmail.com>
date
parent xrwqxwkl
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