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
scan = ARGF.read.lines(chomp: true)

cave = scan.each.with_object({}) {|line, cave|
  line.split(" -> ")
    .map { _1.split(?,).map(&:to_i) }
    .each_cons(2) {|(ax,ay),(bx,by)|
      Range.new(*[ax, bx].sort).each do |x|
        Range.new(*[ay, by].sort).each do |y|
          cave[[x, y]] = ?#
        end
      end
    }
}

def cave.to_s
  x_min, x_max = keys.map(&:first).minmax
  y_min, y_max = keys.map(&:last).minmax

  (0..y_max+1).map {|y|
    (x_min-1..x_max+1).map {|x|
      self.fetch([x, y], ?.)
    }.join
  }.join("\n")
end

def pour_sand(cave, stop:)
  return enum_for(__method__, cave, stop:) unless block_given?

  loop do
    # puts cave
    pos = [500, 0]

    while next_pos = [0, -1, 1].map {|dx| pos.zip([dx, 1]).map { _1 + _2 }}.find { cave[_1].nil? }
      pos = next_pos
      break if stop.(*pos)
    end
    break if stop.(*pos)

    cave[pos] = ?o
    yield pos
  end
end

y_max = cave.keys.map(&:last).max

# part one
p pour_sand(cave, stop: ->(_, y) { y >= y_max }).count

# part two
cave.delete_if { _2 == ?o } # reset cave
cave.default_proc = ->(_,(_,y)) { y == y_max + 2 ? ?# : nil }
p pour_sand(cave, stop: ->(x, y) { [x, y] == [500, 0] }).count + 1