[2023][ruby][10.x]
change txwqlvspuokkxmztuytktkrzqzymvukl
commit 3c1b2e297c2ae12f3299e036172420c19fa322e3
author Alpha Chen <alpha@kejadlen.dev>
date
parent zymnrnzp
diff --git a/2023/ruby/day_10.rb b/2023/ruby/day_10.rb
new file mode 100644
index 0000000..50c25c3
--- /dev/null
+++ b/2023/ruby/day_10.rb
@@ -0,0 +1,104 @@
+require "forwardable"
+
+DIR_DELTAS = {
+  N: [-1,  0],
+  W: [ 0, -1],
+  E: [ 0,  1],
+  S: [ 1,  0],
+}
+TILE_NEIGHBOR_DELTAS = {
+  ?| => "NS",
+  ?- => "EW",
+  ?L => "NE",
+  ?J => "NW",
+  ?7 => "SW",
+  ?F => "SE",
+  ?S => "NEWS",
+}.transform_values {|dirs| dirs.chars.map { DIR_DELTAS.fetch(_1.to_sym) }}
+
+Tiles = Data.define(:tiles) do
+  extend Forwardable
+  def_delegator :tiles, :key
+  def_delegator :tiles, :fetch
+  def_delegator :tiles, :size
+
+  def neighbors(coord)
+    TILE_NEIGHBOR_DELTAS.fetch(self.fetch(coord))
+      .map {|delta| coord.zip(delta).map { _1 + _2 }}
+      .select { tiles.has_key?(_1) }
+  end
+
+  def to_s
+    coords = tiles.keys
+    y_range, x_range = *extents
+    y_range.map {|y|
+      x_range.map {|x| tiles.fetch([y,x], ?.) }.join
+    }.join("\n")
+  end
+
+  def extents
+    y_range = Range.new(*tiles.keys.map(&:first).minmax)
+    x_range = Range.new(*tiles.keys.map(&:last).minmax)
+    [y_range, x_range]
+  end
+end
+
+tiles = ARGF.readlines(chomp: true).each.with_index.inject({}) {|tiles, (row, y)|
+  tiles.merge(
+    row.chars.each.with_index
+      .reject {|tile,_| tile == ?. }
+      .to_h {|tile,x| [[y,x], tile] }
+  )
+}
+tiles = Tiles.new(tiles)
+
+start = tiles.key(?S)
+seed = tiles.neighbors(start)
+  .find {|candidate|
+    tiles.neighbors(candidate).any? { tiles.fetch(_1) == ?S }
+  }
+
+loop_ = [seed]
+until loop_.last == start
+  cur = loop_.last
+  loop_ << tiles.neighbors(cur)
+    .reject { _1 == loop_[-2] }
+    .first
+end
+
+# fix the start tile
+start_dirs = [seed, loop_[-2]].map {|coord| start.zip(coord).map { _2 - _1 }}
+tile = TILE_NEIGHBOR_DELTAS.find { _2.sort == start_dirs.sort }[0]
+tiles.tiles[start] = tile
+
+loop_ = Tiles.new(loop_.to_h { [_1, tiles.fetch(_1)] })
+# puts loop_
+
+# part one
+p loop_.size / 2
+
+# part two
+y_range, x_range = *loop_.extents
+p y_range.map {|y|
+  x_range.chunk_while {|x_a, x_b|
+    a = loop_.fetch([y, x_a], nil)
+    b = loop_.fetch([y, x_b], nil)
+
+    (a.nil? && b.nil?) || (a && TILE_NEIGHBOR_DELTAS.fetch(a).include?([0,1]))
+  }.select {|chunk|
+    # keep empty tiles
+    end_tiles = chunk.values_at(0, -1).map {|x| loop_.fetch([y,x], nil) }
+    next true if end_tiles.all?(&:nil?)
+
+    # only keep pipes that cross the horizontal axis
+    deltas = end_tiles.flat_map {|tile| TILE_NEIGHBOR_DELTAS.fetch(tile) }
+    [[-1,0], [1,0]].all? {|dy| deltas.include?(dy) }
+  }.inject([false, 0]) {|(enclosed, count), chunk|
+    start_tile = loop_.fetch([y,chunk[0]], nil)
+    if start_tile.nil?
+      [enclosed, count + (enclosed ? chunk.size : 0)]
+    else
+      [!enclosed, count]
+    end
+  }
+}.sum(&:last)