1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
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.sum {|y|
  x_range.chunk_while {|x_a, x_b|
    a = loop_.fetch([y, x_a], nil)
    b = loop_.fetch([y, x_b], nil)

    # it's a chunk if it's multiple empty tiles or a pipe that goes east
    (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) }
  }.sum {|chunk|
    tiles = chunk.map {|x| loop_.fetch([y,x], nil) }
    (tiles[0]...tiles[0]) ? tiles.count(nil) : 0 # lol
  }
}