mu
change kkvmkvmpuqqrqpwvnyplzolyxumzyxkp
commit 1df104a18876f32484f5c4c613468228369958c3
author Alpha Chen <alpha@kejadlen.dev>
date
parent znowzzmv
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..f621a5c
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "2022/bqn/lib"]
+	path = 2022/bqn/lib
+	url = https://github.com/mlochbaum/bqn-libs.git
diff --git a/2021/j/day_01.j b/2021/j/day_01.j
new file mode 100755
index 0000000..30e9560
--- /dev/null
+++ b/2021/j/day_01.j
@@ -0,0 +1,4 @@
+#!/usr/bin/env jconsole
+
+echo stdin
+exit''
diff --git a/2021/ruby/day_19.rb b/2021/ruby/day_19.rb
new file mode 100644
index 0000000..1cc64e2
--- /dev/null
+++ b/2021/ruby/day_19.rb
@@ -0,0 +1,52 @@
+require "matrix"
+require "set"
+
+scanners = ARGF.read.strip.split("\n\n").to_h {|scanner|
+  id, *rest = scanner.split("\n")
+  id = id.scan(/\d+/)[0].to_i
+  coords = rest.map { _1.split(?,).map(&:to_i) }
+  [id, coords.map { Matrix.column_vector(_1) }]
+}
+
+first_scanner = scanners.shift
+origin_beacon = first_scanner[1].first
+origin_beacon = Matrix.column_vector([-618,-824,-621])
+
+known_scanners = {
+  first_scanner[0] => [ origin_beacon.map { -_1 }, Matrix.identity(3) ],
+}
+known_beacons = Set.new(first_scanner[1].map {|beacon| beacon - origin_beacon })
+
+rot_x = Matrix[ [1, 0,  0], [0, 0, -1], [0, 1,  0] ]
+rot_y = Matrix[ [0,  0, 1], [0,  1, 0], [-1, 0, 0] ]
+rot_z = Matrix[ [0, -1, 0], [1,  0, 0], [0,  0, 1] ]
+id = Matrix.identity(3)
+
+ROTATIONS = Set.new(
+  Array.new(4) { rot_x ** _1 }.flat_map {|x|
+    Array.new(4) { rot_y ** _1 }.flat_map {|y|
+      Array.new(4) { rot_z ** _1 }.map {|z| x * y * z }}}
+)
+
+def find_overlapping_scanner(known_beacons, scanners)
+  scanners.filter_map {|id, beacons|
+    haystack = ROTATIONS.flat_map {|r| beacons.map {|b| [ r*b, r ] }}
+    # haystack.select { _2 == Matrix.identity(3) }.map(&:first).each { p _1 }
+    haystack.find {|position, rotation|
+      translated_beacons = beacons.map { (rotation * _1) - position }
+      (known_beacons & translated_beacons).size >= 12
+    }&.then {|p,o| [id, p, o] }
+  }.first
+end
+
+until scanners.empty?
+  id, position, orientation = find_overlapping_scanner(known_beacons, scanners)
+
+  p [id, position, orientation]
+
+  known_scanners[id] = [position, orientation]
+  translated_beacons = scanners[id].map {|b| b.zip(position).map { _2 - _1 }.zip(orientation).map { _1 * _2 }}
+  known_beacons.merge(translated_beacons)
+  scanners.delete(id)
+end
+
diff --git a/2021/ruby/day_23.rb b/2021/ruby/day_23.rb
new file mode 100644
index 0000000..10e624a
--- /dev/null
+++ b/2021/ruby/day_23.rb
@@ -0,0 +1,35 @@
+#############
+#...........#
+###B#C#B#D###
+  #A#D#C#A#
+  #########
+
+#############
+#...........#
+###D#D#A#A###
+  #C#C#B#B#
+  #########
+
+State = Struct.new(:spaces, :energy) do
+  ENERGY = { A: 1, B: 10, C: 100, D: 1000 }.transform_keys(&:to_s)
+  ROOMS = [2, 4, 6, 8]
+
+  def rooms = ROOMS.to_h { [_1, spaces.fetch(_1)] }
+
+  def valid_moves
+
+  end
+end
+
+spaces = Array.new(11) { [] }
+# input = "DC DC AB AB"
+input = "BA CD BC DA"
+input.split(" ").map(&:chars).each.with_index do |amphipods, i|
+  spaces[(i+1) * 2] = amphipods
+end
+
+start = State.new(spaces, 0)
+
+p start
+p start.rooms
+p start.valid_moves
diff --git a/2021/ruby/day_24.rb b/2021/ruby/day_24.rb
new file mode 100644
index 0000000..0d78cc7
--- /dev/null
+++ b/2021/ruby/day_24.rb
@@ -0,0 +1,58 @@
+INSTRUCTIONS = ARGF.read.split("\n").map { _1.split(/\s+/) }
+
+# INSTRUCTIONS.each do |instruction, *args|
+#   a, b = *args
+#   case instruction
+#   when "inp"
+#     puts "#{a} = input.shift"
+#   when "add"
+#     puts "#{a} += #{b}"
+#   when "mul"
+#     puts "#{a} *= #{b}"
+#   when "div"
+#     puts "#{a} = (#{a} / #{b}.to_f).floor"
+#   when "mod"
+#     puts "#{a} %= #{b}"
+#   when "eql"
+#     puts "#{a} = #{a} == #{b} ? 1 : 0"
+#   else
+#     fail
+#   end
+# end
+# exit
+
+VARS = %w[ w x y z ]
+def run(input)
+  vars = Hash.new(0)
+
+  INSTRUCTIONS.each do |instruction, *args|
+    a, b = *args
+    b = VARS.include?(b) ? vars[b] : b.to_i
+    case instruction
+    when "inp"
+      vars[a] = input.shift
+    when "add"
+      vars[a] += b
+    when "mul"
+      vars[a] *= b
+    when "div"
+      vars[a] = (vars[a] / b.to_f).floor
+    when "mod"
+      vars[a] %= b
+    when "eql"
+      vars[a] = vars[a] == b ? 1 : 0
+    else
+      fail
+    end
+  end
+
+  vars
+end
+
+99999999999999.downto(11111111111111).lazy.map(&:to_s).reject { _1.include?(?0) }.each do |input|
+  vars = run(input.chars.map(&:to_i))
+  if vars[?z].zero?
+    puts input
+    exit
+  end
+end
diff --git a/2021/ruby/day_24.x.rb b/2021/ruby/day_24.x.rb
new file mode 100644
index 0000000..2f0ea2d
--- /dev/null
+++ b/2021/ruby/day_24.x.rb
@@ -0,0 +1,262 @@
+def run(input)
+  w, x, y, z = 0, 0, 0, 0
+
+  # w = input.shift
+  # x = z % 26
+  # z = (z / 1.to_f).floor
+  # x += 13
+  # x = x == w ? 0 : 1
+  # y = 25 * x + 1 # either y = 26 or y = 1
+  # z *= y # 0
+  # # p [w, x, y, z]
+
+  # y = (w + 13) * x
+  # z += y
+  # # p [w, x, y, z]
+  # # puts
+
+  # w = input.shift
+  # x = z % 26
+  # z = (z / 1.to_f).floor
+  # x += 11
+  # x = x == w ? 0 : 1
+  # y = 25 * x + 1
+  # z *= y
+  # # p [w, x, y, z]
+
+  # y = (w + 10) * x
+  # z += y
+  # # p [w, x, y, z]
+  # # puts
+
+  # w = input.shift
+  # x = z % 26
+  # z = (z / 1.to_f).floor
+  # x += 15
+  # x = x == w ? 0 : 1
+  # y = (25 * x) + 1
+  # z *= y
+  # # p [w, x, y, z]
+
+  # y = (w + 5) * x
+  # z += y
+  # # p [w, x, y, z]
+  # # puts
+
+  # w = input.shift
+  # x = z % 26
+  # z = (z / 26.to_f).floor
+  # x += -11
+  # x = x == w ? 0 : 1
+  # y = (25 * x) + 1
+  # z *= y
+  # # p [w, x, y, z]
+
+  # y = (w + 14) * x
+  # z += y
+  # # p [w, x, y, z]
+  # # puts
+
+  # return [w, x, y, z]
+
+  # w = input.shift
+  # x = z % 26
+  # z = (z / 1.to_f).floor
+  # x += 14
+  # x = x == w ? 0 : 1
+  # y = 25 * x + 1
+  # z *= y
+  # # p [w, x, y, z]
+
+  # y = (w + 5) * x
+  # z += y
+  # # p [w, x, y, z]
+  # # puts
+  # # puts
+
+  # w = input.shift
+  # x = z % 26
+  # z = (z / 26.to_f).floor
+  # x += 0
+  # x = x == w ? 0 : 1
+  # y = 25 * x + 1
+  # z *= y
+  # # p [w, x, y, z]
+
+  # y = (w + 15) * x
+  # z += y
+  # # p [w, x, y, z]
+  # # puts
+
+  # return [w, x, y, z]
+
+  # w = input.shift
+  # x = z % 26
+  # z = (z / 1.to_f).floor
+  # x += 12
+  # x = x == w ? 0 : 1
+  # y = 25 * x + 1
+  # z *= y
+  # # p [w, x, y, z]
+
+  # y *= 0
+  # y += w
+  # y += 4
+  # y *= x
+  # z += y
+  # # p [w, x, y, z]
+
+  # w = input.shift
+  # x *= 0
+  # x += z
+  # x %= 26
+  # z = (z / 1.to_f).floor
+  # x += 12
+  # x = x == w ? 1 : 0
+  # x = x == 0 ? 1 : 0
+  # y *= 0
+  # y += 25
+  # y *= x
+  # y += 1
+  # z *= y
+  # # p [w, x, y, z]
+
+  # y *= 0
+  # y += w
+  # y += 11
+  # y *= x
+  # z += y
+  # # p [w, x, y, z]
+
+  # w = input.shift
+  # x *= 0
+  # x += z
+  # x %= 26
+  # z = (z / 1.to_f).floor
+  # x += 14
+  # x = x == w ? 1 : 0
+  # x = x == 0 ? 1 : 0
+  # y *= 0
+  # y += 25
+  # y *= x
+  # y += 1
+  # z *= y
+  # # p [w, x, y, z]
+
+  # y *= 0
+  # y += w
+  # y += 1
+  # y *= x
+  # z += y
+  # # p [w, x, y, z]
+  # # puts
+  # # puts
+
+  # w = input.shift
+  # x *= 0
+  # x += z
+  # x %= 26
+  # z = (z / 26.to_f).floor
+  # x += -6
+  # x = x == w ? 1 : 0
+  # x = x == 0 ? 1 : 0
+  # y *= 0
+  # y += 25
+  # y *= x
+  # y += 1
+  # z *= y
+  # # p [w, x, y, z]
+
+  # y *= 0
+  # y += w
+  # y += 15
+  # y *= x
+  # z += y
+  # # p [w, x, y, z]
+
+  # return [w, x, y, z]
+
+  # w = input.shift
+  # x *= 0
+  # x += z
+  # x %= 26
+  # z = (z / 26.to_f).floor
+  # x += -10
+  # x = x == w ? 1 : 0
+  # x = x == 0 ? 1 : 0
+  # y *= 0
+  # y += 25
+  # y *= x
+  # y += 1
+  # z *= y
+  # # p [w, x, y, z]
+
+  # y *= 0
+  # y += w
+  # y += 12
+  # y *= x
+  # z += y
+  # # p [w, x, y, z]
+
+  # return [w, x, y, z]
+
+  w = input.shift
+  x *= 0
+  x += z
+  x %= 26
+  z = (z / 26.to_f).floor
+  x += -12
+  x = x == w ? 1 : 0
+  x = x == 0 ? 1 : 0
+  y *= 0
+  y += 25
+  y *= x
+  y += 1
+  z *= y
+  # p [w, x, y, z]
+
+  y *= 0
+  y += w
+  y += 8
+  y *= x
+  z += y
+  # p [w, x, y, z]
+
+  return [w, x, y, z]
+
+  w = input.shift
+  x = z % 26
+  z = (z / 26.to_f).floor
+  x += -3
+  x = x == w ? 0 : 1
+  y = 25 * x + 1
+  z *= y
+  # p [w, x, y, z]
+
+  y = (w + 14) * x
+  z += y
+  # p [w, x, y, z]
+
+  w = input.shift
+  x = z % 26              # z - 5 must be w
+  z = (z / 26.to_f).floor # z must be < 26
+  x += -5                 # w must be x - 5
+  x = x == w ? 0 : 1      # x must be w
+  y = 25 * x + 1
+  z *= y                  # z must be 0
+  # p [w, x, y, z]
+
+  y = (w + 9) * x         # x must be 0
+  z += y                  # y must be 0
+
+  [w, x, y, z]
+end
+
+# 99999999999999.downto(11111111111111).lazy.map(&:to_s).reject { _1.include?(?0) }.each do |input|
+  vars = run(input.chars.map(&:to_i))
+  p [input, vars[3]] if vars[3] < 400
+  if vars[3].zero?
+    puts input
+    exit
+  end
+end
diff --git a/2022/bqn/lib b/2022/bqn/lib
new file mode 160000
index 0000000..b90e26c
--- /dev/null
+++ b/2022/bqn/lib
@@ -0,0 +1 @@
+Subproject commit b90e26c29b1742bf164b6121275b5e1ee8a56365
diff --git a/2022/fnl/day_01.fnl b/2022/fnl/day_01.fnl
new file mode 100644
index 0000000..252a095
--- /dev/null
+++ b/2022/fnl/day_01.fnl
@@ -0,0 +1,19 @@
+(fn dbg [tbl]
+  (each [_ chunk (ipairs tbl)]
+    (print (accumulate [x "" _ i (ipairs chunk)]
+             (.. x ", " i)))))
+
+(local input (accumulate [input [[]] line (io.lines :../day_01.txt)]
+               (do
+                 (if (= (length line) 0)
+                     (table.insert input [])
+                     (table.insert (. input (length input)) (tonumber line)))
+                 input)))
+
+(fn sum [l]
+  (accumulate [sum 0 _ n (ipairs l)]
+    (+ sum n)))
+
+(local l (icollect [_ l (ipairs input)] (sum l)))
+(table.sort l)
+(print (. l (length l)))
diff --git a/2022/ruby/.ruby-version b/2022/ruby/.ruby-version
index 8c50098..a3ec5a4 100644
--- a/2022/ruby/.ruby-version
+++ b/2022/ruby/.ruby-version
@@ -1 +1 @@
-3.1
+3.2
diff --git a/2022/ruby/day_17.rb b/2022/ruby/day_17.rb
new file mode 100644
index 0000000..6609b7c
--- /dev/null
+++ b/2022/ruby/day_17.rb
@@ -0,0 +1,89 @@
+require "set"
+
+rocks = <<~ROCKS.split("\n\n")
+  ####
+
+  .#.
+  ###
+  .#.
+
+  ..#
+  ..#
+  ###
+
+  #
+  #
+  #
+  #
+
+  ##
+  ##
+ROCKS
+rocks = rocks
+  .map {|rock| rock.lines(chomp: true).reverse.map(&:chars) }
+  .map {|rock|
+    rock.each.with_index.with_object(Set.new) {|(row, y), rocks|
+      row.each.with_index do |c, x|
+        rocks << [x, y] if c == ?#
+      end
+    }
+  }
+rocks = rocks.cycle
+
+jet_pattern = ARGF.read.strip.chars
+jet_pattern = jet_pattern.cycle
+
+chamber = Hash.new {|_,(x,y)| !(0...7).cover?(x) || y.negative? }
+
+def move_rock(chamber, rock, dx, dy)
+  next_rock = rock.map {|x,y| [x+dx, y+dy] }
+
+  if next_rock.any? {|x,y| chamber[[x, y]] }
+    rock
+  else
+    next_rock
+  end
+end
+
+def debug(chamber, rock)
+  max_y = [chamber.keys.map(&:last).max || 0, rock.map(&:last).max || 0].max
+
+  puts
+  puts max_y.downto(0).map {|y|
+    (0...7).map {|x|
+      (chamber[[x,y]] || rock.include?([x,y])) ? ?# : ?.
+    }.join
+  }.join("\n")
+end
+
+2022.times do
+  rock = rocks.next
+
+  y = chamber.empty? ? 3 : chamber.keys.map(&:last).max + 4
+  x = 2
+  rock = move_rock(chamber, rock, x, y)
+
+  loop do
+    rock = case jet = jet_pattern.next
+           when ?< then move_rock(chamber, rock, -1, 0)
+           when ?> then move_rock(chamber, rock,  1, 0)
+           else fail "invalid jet pattern: #{jet}"
+           end
+
+    next_rock = move_rock(chamber, rock, 0, -1)
+    break if rock == next_rock
+    rock = next_rock
+  end
+
+  chamber.merge!(rock.to_h { [_1, true] })
+end
+
+# debug(chamber, Set.new)
+max_y = chamber.keys.map(&:last).max + 1
+p max_y
+
+candidates = (0..max_y).select {|y| (0...7).count {|x| chamber[[x,y]] } == 6 }
+# max_delta = candidates.each_cons(2).map { _2 - _1 }.max
+candidates.
+p candidates
+p max_delta
diff --git a/2022/ruby/day_19.rb b/2022/ruby/day_19.rb
new file mode 100644
index 0000000..f9d0e92
--- /dev/null
+++ b/2022/ruby/day_19.rb
@@ -0,0 +1,81 @@
+SEEN = []
+
+class Factory
+  attr_reader :time, :robots, :resources
+
+  def initialize(
+    blueprint,
+    time=0,
+    robots={ ore: 1, clay: 0, obsidian: 0, geode: 0 },
+    resources={ ore: 0, clay: 0, obsidian: 0, geode: 0 }
+  )
+    @blueprint = blueprint
+    @time = time
+    @robots = robots
+    @resources = resources
+  end
+
+  def max_geodes
+    if @time == 20
+      pp self if @robots.values_at(:ore, :clay, :obsidian, :geode) == [1, 4, 2, 1]
+      return @resources.fetch(:geode)
+    end
+
+    resources = @resources.merge(@robots) { _2 + _3 }
+    branches = @blueprint.filter_map {|robot, cost|
+      if cost.all? {|res, qty| @resources.fetch(res) >= qty }
+        Factory.new(
+          @blueprint,
+          @time + 1,
+          @robots.merge({robot => 1}) { _2 + _3 },
+          resources.merge(cost) { _2 - _3 },
+        )
+      else
+        nil
+      end
+    }
+
+    if @blueprint.any? {|_, cost| cost.any? {|res, qty| @robots.fetch(res) > 0 && @resources.fetch(res) < qty }}
+      pp self
+      branches << Factory.new(@blueprint, @time + 1, @robots, resources)
+    end
+
+    branches = branches.reject {|factory|
+      SEEN.any? {|other|
+        %i[ ore clay obsidian geode ].all? {|res|
+          factory.robots.fetch(res) <= other.robots.fetch(res) &&
+          factory.resources.fetch(res) <= other.resources.fetch(res) &&
+          factory.time >= other.time
+        }
+      }
+    }
+    SEEN.concat(branches)
+
+    return @resources.fetch(:geode) if branches.empty?
+
+    branches.map(&:max_geodes).max
+  end
+
+  def to_s
+    "<time: #@time robots: #{@robots.values} resources: #{@resources.values}>"
+  end
+  alias_method :inspect, :to_s
+end
+
+blueprints = ARGF.read
+  .split("\n\n").map { _1.lines(chomp: true).join }
+  .map { _1.scan(/\d+/).map(&:to_i) }
+  .map {
+    _1.shift # id
+    {
+      ore: {ore: _1.shift },
+      clay: {ore: _1.shift },
+      obsidian: {ore: _1.shift, clay: _1.shift },
+      geode: {ore: _1.shift, obsidian: _1.shift },
+    }
+  }
+
+p Factory.new(blueprints[0]).max_geodes
+p SEEN.count
+
+__END__
diff --git a/2022/ruby/day_22.rb b/2022/ruby/day_22.rb
new file mode 100644
index 0000000..faa42b4
--- /dev/null
+++ b/2022/ruby/day_22.rb
@@ -0,0 +1,78 @@
+require "strscan"
+
+include Math
+
+map, path = ARGF.read.chomp.split("\n\n")
+
+map = map.split("\n").each.with_index.with_object({}) {|(row,y),map|
+  row.chars.each.with_index do |tile,x|
+    map[[y,x]] = tile if tile != " "
+  end
+}
+max_y = map.keys.map(&:first).max
+max_x = map.keys.map(&:last).max
+
+start_x = map.keys.select { _1.first.zero? }.map(&:last).min
+
+current = [0, start_x]
+facing = 0
+
+ss = StringScanner.new(path)
+until ss.eos?
+  case
+  when n = ss.scan(/\d+/)
+    n = n.to_i
+    n.times do
+      y, x = current.zip([-sin(facing),cos(facing)]).map { _1 + _2 }.map(&:to_i)
+      next_tile = map[[y,x]]
+
+      unless next_tile
+        puts
+        p [y,x, facing]
+        y, x = case facing
+               when 0      then [y, 0]
+               when PI/2   then [max_y, x]
+               when PI     then [y, max_x]
+               when 3*PI/2 then [0, x]
+               else        fail "unexpected facing: #{facing}"
+               end
+        next_tile = map[[y,x]]
+        while next_tile.nil?
+          y, x = [y, x].zip([-sin(facing),cos(facing)]).map { _1 + _2 }.map(&:to_i)
+          next_tile = map[[y,x]]
+        end
+
+        p [y,x,next_tile]
+      end
+
+      case next_tile
+      when ?.
+        current = [y,x]
+      when ?#
+        break
+      else
+        fail "unexpected tile: #{next_tile}"
+      end
+    end
+  when dir = ss.scan(/R|L/)
+    facing += case dir
+              when ?R then -PI/2
+              when ?L then  PI/2
+              else          fail "unexpected dir: #{dir}"
+              end
+    facing %= 2*PI
+  else
+    fail ss.rest
+  end
+end
+
+row, col = current.map { _1 + 1 }
+facing = case facing
+         when 0      then 0
+         when PI/2   then 3
+         when PI     then 2
+         when 3*PI/2 then 2
+         else
+           fail "unexpected facing: #{facing}"
+         end
+p [1000, 4, 1].zip([row, col, facing]).sum { _1 * _2 }
diff --git a/2022/ruby/day_24.rb b/2022/ruby/day_24.rb
new file mode 100644
index 0000000..d13a393
--- /dev/null
+++ b/2022/ruby/day_24.rb
@@ -0,0 +1,93 @@
+require "set"
+
+walls = Set.new
+blizzards = Hash.new {|h,k| h[k] = Set.new }
+clear_ground = Set.new
+
+ARGF.read.lines(chomp: true).each.with_index do |row, y|
+  row.chars.each.with_index do |pos,x|
+    case pos
+    when ?#
+      walls.add([y,x])
+    when /[v^<>]/
+      blizzards[pos].add([y,x])
+    when ?.
+      clear_ground.add([y,x])
+    else
+      fail pos
+    end
+  end
+end
+
+x_range = Range.new(*walls.map(&:last).minmax)
+y_range = Range.new(*walls.map(&:first).minmax)
+
+debug = ->() do
+  puts
+  puts y_range.map {|y|
+    x_range.map {|x|
+      if walls.include?([y,x])
+        ?#
+      elsif blizzards.any? {|_,pos| pos.include?([y,x])} 
+        b = blizzards.select {|_,pos| pos.include?([y,x])}
+        (b.size == 1) ? b.keys[0] : b.size
+      else
+        ?.
+      end
+    }.join
+  }.join("\n")
+end
+
+start = [0, clear_ground.find {|y,x| y == 0 }.last]
+goal = [y_range.end, clear_ground.find {|y,x| y == y_range.end }.last]
+
+dir_deltas = {
+  ?^ => [-1,  0],
+  ?v => [ 1,  0],
+  ?< => [ 0, -1],
+  ?> => [ 0,  1],
+}
+moves = [*dir_deltas.values, [0, 0]]
+
+frontier = [start]
+(1..).each do |time|
+  if (time % 1).zero?
+    puts time
+    # p frontier
+    # debug.()
+  end
+
+  blizzards = blizzards.to_h {|dir,positions|
+    delta = dir_deltas.fetch(dir)
+    edges, positions = positions
+      .map {|pos| pos.zip(delta).map { _1 + _2 }}
+      .partition { walls.include?(_1) }
+    positions.concat(edges.map {|(y,x)|
+      case dir
+      when ?^ then [y_range.end - 1, x]
+      when ?v then [y_range.begin + 1, x]
+      when ?< then [y, x_range.end - 1]
+      when ?> then [y, x_range.end + 1]
+      else fail dir
+      end
+    })
+    [dir, positions]
+  }
+
+  clear_ground = y_range.map {|y| x_range.map {|x| [y, x] }}.flatten(1)
+    .reject {|pos| walls.include?(pos) || blizzards.any? { _2.include?(pos) }}
+
+  options = frontier.map {|current|
+    moves.filter_map {|delta|
+      pos = current.zip(delta).map { _1 + _2 }
+      clear_ground.include?(pos) && pos
+    }
+  }.flatten(1)
+
+  frontier = options.uniq
+
+  if frontier.include?(goal)
+    puts time + 1
+    exit
+  end
+end
diff --git a/etc/total-time.rb b/etc/total-time.rb
new file mode 100644
index 0000000..0193583
--- /dev/null
+++ b/etc/total-time.rb
@@ -0,0 +1,12 @@
+seconds = ARGF.read.strip.lines(chomp: true)
+  .sum {|line|
+    line.strip.split(/\s+/).fetch(4)
+      .split(?:).map(&:to_i)
+      .zip([60*60, 60, 1])
+      .sum { _1 * _2 }
+  }
+
+hours, seconds = seconds.divmod(60 * 60)
+minutes, seconds = seconds.divmod(60)
+
+puts "#{hours} hours, #{minutes} minutes, #{seconds} seconds"