Whee, it’s time for Advent of Code again! 37/24 for Day 1. We’ll see how often I’ll be able to leaderboard this year.
# p ARGF.read.split("\n\n").map { _1.each_line.map(&:to_i).sum }.max
p ARGF.read.split("\n\n").map { _1.each_line.map(&:to_i).sum }.sort[-3..-1].sum
I always find the code for my leaderboarding attempts (especially the successful ones) to be an interesting look into both my initial instincts for breaking down a problem, as well as which parts of Ruby I use to solve it.
      -------Part 1--------   -------Part 2--------
Day       Time  Rank  Score       Time  Rank  Score
  3   00:04:06   290      0   00:06:07   158      0
priorities = ((?a..?z).to_a + (?A..?Z).to_a).map.with_index { [_1, _2+1]  }.to_h
# part 1
# p ARGF.read.lines(chomp: true).map {|line|
#   len = line.length
#   a = line[0...len/2]
#   b = line[len/2..]
#   priorities.fetch((a.chars & b.chars)[0])
# }.sum
p ARGF.read.lines(chomp: true).each_slice(3).map {|chunk|
  priorities.fetch(chunk.map(&:chars).inject(&:&)[0])
}.sum
      -------Part 1--------   -------Part 2--------
Day       Time  Rank  Score       Time  Rank  Score
  4   00:03:18   452      0   00:03:37   147      0
p ARGF.read.lines(chomp: true)
  .map { _1.split(?,) }
  .map {|x| x.map { _1.split(?-).map(&:to_i) }}
  .map {|(a,b),(x,y)| [(a..b), (x..y)] }
  # .count {|a,b| (a.cover?(b.begin) && a.cover?(b.end)) || (b.cover?(a.begin) && b.cover?(a.end)) }
  .count {|a,b| (a.cover?(b.begin) || a.cover?(b.end)) || (b.cover?(a.begin) || b.cover?(a.end)) }
      -------Part 1--------   -------Part 2--------
Day       Time  Rank  Score       Time  Rank  Score
  5   00:07:57   216      0   00:09:52   230      0
setup, moves = ARGF.read.split("\n\n")
setup = setup
  .lines(chomp: true)
  .map(&:chars)
  .transpose
  .map {|col| col.select { _1 =~ /[A-Z]/ }}
  .reject(&:empty?)
moves = moves.scan(/move (\d+) from (\d+) to (\d+)/).map { _1.map(&:to_i) }
moves.each do |n,from,to|
  # n.times {
  #   setup[to-1].unshift(setup[from-1].shift)
  # }
  setup[to-1].unshift(*setup[from-1].shift(n))
end
p setup.map(&:first).join
      -------Part 1--------   -------Part 2--------
Day       Time  Rank  Score       Time  Rank  Score
  6   00:04:58  1987      0   00:05:25  1413      0
# p ARGF.read.chars.each_cons(4).with_index.find {|a,i| a.uniq.size == 4 }.last + 4
p ARGF.read.chars.each_cons(14).with_index.find {|a,i| a.uniq.size == 14 }.last + 14
Having fun playing with BQN for Advent of Code this year!
# •Show 4 + ⊐⟜1 ⍷⊸≡¨ <˘ 4↕ •FChars "../day_06.txt"
•Show 14 + ⊐⟜1 ⍷⊸≡¨ <˘ 14↕ •FChars "../day_06.txt"
Oof, that one was rough - my approach for part one was not usable at all for part two.
      -------Part 1--------   -------Part 2--------
Day       Time  Rank  Score       Time  Rank  Score
  8   00:22:03  3656      0   00:36:40  3129      0
# grid = ARGF.read.lines(chomp: true).map {|row| row.chars.map { [_1.to_i, false] }}
# def mark!(ary)
#   max = -1
#   ary.each do |tree_counted|
#     if tree_counted.first > max
#       max = tree_counted.first
#       tree_counted[1] = true
#     end
#   end
# end
# grid.each { mark!(_1) }
# grid.each { mark!(_1.reverse) }
# grid.transpose.each { mark!(_1) }
# grid.transpose.each { mark!(_1.reverse) }
# p grid.sum { _1.count(&:last) }
grid = ARGF.read.lines(chomp: true).map { _1.chars.map(&:to_i) }
def score(grid, y, x)
  tree = grid[y][x]
  [
    (0...y).to_a.reverse.slice_after {|i| grid[i][x] >= tree }.first,
    (y+1...grid.size).slice_after {|i| grid[i][x] >= tree }.first,
    (0...x).to_a.reverse.slice_after {|i| grid[y][i] >= tree }.first,
    (x+1...grid.size).slice_after {|i| grid[y][i] >= tree }.first,
  ].compact.map(&:size).inject(:*)
end
p grid.flat_map.with_index {|row,y|
  row.map.with_index {|tree,x|
    score(grid, y, x)
  }
}.max
Refactoring so the logic is clearer:
grid = ARGF.read.lines(chomp: true).map { _1.chars.map(&:to_i) }
def each(grid)
  return enum_for(__method__, grid) unless block_given?
  transposed = grid.transpose
  grid.each.with_index do |row, y|
    row.each.with_index do |tree, x|
      col = transposed[x]
      sight_lines = [
        (0...x).map { row[_1] }.reverse,
        (x+1...row.size).map { row[_1] },
        (0...y).map { col[_1] }.reverse,
        (y+1...row.size).map { col[_1] },
      ]
      yield tree, sight_lines
    end
  end
end
p each(grid).count {|tree, sight_lines|
  sight_lines.any? { _1.empty? || tree > _1.max }
}
p each(grid).map {|tree, sight_lines|
  sight_lines
    .map {|sl| sl.slice_after { _1 >= tree }.first }
    .compact
    .map(&:size)
    .inject(:*)
}.max
@alpha You are human after all! I think this may be the only one I've ever finished before you!
There’s a lesson here in that the refactored version, which shares logic between the solutions, is barely shorter than the two independent implementations that don’t share any logic at all. The abstraction doesn’t make the code any shorter, but it does unify the approach to solving both parts.
I don’t have a pithy lesson here or anything, but thought it was an interesting parallel to real-world development, which doesn’t often show up in my Advent of Code solutions.
      -------Part 1--------   -------Part 2--------
Day       Time  Rank  Score       Time  Rank  Score
  9   00:22:45  2529      0   00:26:50  1006      0
require "set"
motions = ARGF.read.scan(/([RLUD])\s+(\d+)/).map { [_1, _2.to_i] }
class Snake
  def initialize
    @knots = Array.new(10) { [0, 0] }
  end
  def tail = @knots.last
  def move!(dir)
    delta = case dir
            when ?L then [ 0, -1]
            when ?U then [ 1,  0]
            when ?R then [ 0,  1]
            when ?D then [-1,  0]
            else fail dir.inspect
            end
    @knots[0] = @knots[0].zip(delta).map { _1 + _2 }
    @knots = @knots[1..].inject([@knots[0]]) {|knots, tail|
      head = knots.last
      delta = head.zip(tail).map { _1 - _2 }
      knots << if delta.any? { _1.abs > 1 }
                 tail.zip(delta.map { _1.clamp(-1, 1) }).map { _1 + _2 }
               else
                 tail
               end
    }
  end
end
snake = Snake.new
seen = Set.new
motions.each do |dir, distance|
  distance.times do
    snake.move!(dir)
    seen << snake.tail
  end
end
p seen.size
      -------Part 1--------   -------Part 2--------
Day       Time  Rank  Score       Time  Rank  Score
 10   00:28:35  6047      0   00:36:57  3289      0
Whew, this one took me a while to just grok! Not even going to bother doing any refactoring, ugh.
instructions = ARGF.read.lines(chomp: true)
adds = {}
cycle = 1
instructions.each do |instruction|
  cycle += case instruction
           when /noop/
             1
           when /addx (-?\d+)/
             adds[cycle+1] = $1.to_i
             2
           else fail instruction
           end
end
xs = (0..adds.keys.max).inject([1]) { _1 << _1.last + adds.fetch(_2, 0) }
p 20.step(by: 40, to: 220).map { [_1, xs[_1]] }.sum { _1 * _2 }
puts 6.times.map {|y|
  40.times.map {|x|
    cycle = 40*y + x + 1
    xx = xs.fetch(cycle, xs.last)
    (-1..1).cover?(x - xx) ? ?# : ?.
  }.join
}.join("\n")
The most obtuse code I’ve written so far for Advent of Code, for sure. But still shockingly concise. Feel like there must be a better way to get xs, perhaps even combining the two loops, but I don’t think I really have the brains for it right now.
@robdaemon @alpha it’s MatzLisp!
Back to a relatively decent position on the global leaderboard! Only somewhat gross code, mostly in the parsing.
      -------Part 1--------   -------Part 2--------
Day       Time  Rank  Score       Time  Rank  Score
 11   00:16:07   233      0   00:20:48   167      0
Monkey = Struct.new(:id, :items, :operation, :test, :throw_to)
monkeys = ARGF.read.split("\n\n").map {|raw|
  raw = raw.lines(chomp: true)
  id = raw.shift.match(/\d+/)[0].to_i
  starting_items = raw.shift.match(/: (.+)/)[1].split(", ").map(&:to_i)
  operation_ = raw.shift.match(/: new = (.+)/)[1]
  operation = ->(old) { eval(operation_) }
  test = raw.shift.match(/\d+/)[0].to_i
  t = raw.shift.match(/\d+/)[0].to_i
  f = raw.shift.match(/\d+/)[0].to_i
  throw_to = ->(n) { (n % test).zero? ? t : f }
  Monkey.new(id, starting_items, operation, test, throw_to)
}
max_worry = monkeys.map(&:test).inject(:*)
inspections = Hash.new(0)
# 20.times do
10_000.times do
  monkeys.each do |monkey|
    until monkey.items.empty?
      inspections[monkey.id] += 1
      item = monkey.items.shift
      item = monkey.operation.(item)
      # item /= 3
      item %= max_worry
      to = monkey.throw_to.(item)
      monkeys[to].items << item
    end
  end
end
p inspections.values.max(2).inject(:*)
@robdaemon I think a lot of the Ruby I write tends to be in a relatively functional style. In some ways, starting to learn an array programming language (BQN) is feeling somewhat familiar from how comfortable I am with enumerables/iterators in Ruby/Rust.
This is a little cleaner, but so much more fragile.
monkeys = ARGF.read.scan(/Monkey (\d+):
  Starting items: ((?~\n))
  Operation: new = ((?~\n))
  Test: divisible by (\d+)
    If true: throw to monkey (\d+)
    If false: throw to monkey (\d+)
/m).map {|id, items, operation, test, t, f|
  Monkey.new(
    id.to_i,
    items.split(", ").map(&:to_i),
    operation,
    test.to_i,
    t.to_i,
    f.to_i,
  )
}
@alpha That is really cool. I spent 45 minutes writing a parser for this in Java... not a language built for rapid prototyping or text parsing!
Parslet is nice.
class MonkeyParser < Parslet::Parser
  rule(:num) { match("[0-9]").repeat.as(:num) }
  rule(:space) { match("\\s").repeat }
  rule(:newline) { str("\n") }
  rule(:monkey) {
    space >> str("Monkey ") >> num.as(:id) >> str(":") >>
    space >> str("Starting items: ") >> ( num >> ( str(", ") >> num ).repeat ).as(:items) >>
    space >> str("Operation: new = ") >> (newline.absent? >> any).repeat.as(:op) >>
    space >> str("Test: divisible by ") >> num.as(:test) >>
    space >> str("If true: throw to monkey ") >> num.as(:true) >>
    space >> str("If false: throw to monkey ") >> num.as(:false) >>
    space
  }
  rule(:monkeys) { monkey.repeat }
  root(:monkeys)
end
class MonkeyTransform < Parslet::Transform
  rule(num: simple(:x)) { x.to_i }
  rule(
    id: simple(:id),
    items: sequence(:items),
    op: simple(:op),
    test: simple(:test),
    true: simple(:t),
    false: simple(:f),
  ) {
    Monkey.new(id, items, op, test, t, f)
  }
end
      -------Part 1--------   -------Part 2--------
Day       Time  Rank  Score       Time  Rank  Score
 12   00:20:13  1031      0   00:28:51  1414      0
Finished with this code here:
height_map = ARGF.read.lines(chomp: true).map(&:chars)
  .each.with_index.with_object({}) do |(row,y),map|
    row.each.with_index do |height,x|
      map[[y,x]] = height
    end
  end
s, e = height_map.invert.values_at(?S, ?E)
height_map[s] = ?a
height_map[e] = ?z
# part one
# queue = [s]
# visited = { s => 0 }
# until visited.has_key?(e)
#   current = queue.shift
#   moves = visited.fetch(current)
#   neighbors = [
#     [-1,  0],
#     [ 1,  0],
#     [ 0, -1],
#     [ 0,  1],
#   ].map { current.zip(_1).map {|a,b| a + b } }
#     .select { height_map.has_key?(_1) }
#     .reject { visited.has_key?(_1) }
#     .select { height_map.fetch(_1).ord - 1 <= height_map.fetch(current).ord }
#   neighbors.each do |y,x|
#     visited[[y,x]] = moves + 1
#     queue << [y,x]
#   end
#   queue.sort_by { visited.fetch(_1) }
# end
# p visited.fetch(e)
# part two
queue = [e]
visited = { e => 0 }
loop do
  current = queue.shift
  moves = visited.fetch(current)
  neighbors = [
    [-1,  0],
    [ 1,  0],
    [ 0, -1],
    [ 0,  1],
  ].map { current.zip(_1).map {|a,b| a + b } }
    .select { height_map.has_key?(_1) }
    .reject { visited.has_key?(_1) }
    .select { height_map.fetch(current).ord <= height_map.fetch(_1).ord + 1 }
  if a = neighbors.find { height_map.fetch(_1) == ?a }
    p moves + 1
    break
  end
  neighbors.each do |y,x|
    visited[[y,x]] = moves + 1
    queue << [y,x]
  end
  queue.sort_by { visited.fetch(_1) }
end
Refactored to be nicer and smarter:
class HeightMap
  NEIGHBORS = [[0, -1], [-1, 0], [0, 1], [1, 0]]
  def initialize(heights)
    @heights = heights
  end
  def shortest(from:, to:, &cond)
    frontier = [from]
    visited = { from => 0 }
    until frontier.empty? || to.any? { visited.has_key?(_1) }
      current = frontier.shift
      NEIGHBORS.each do |delta|
        candidate = current.zip(delta).map { _1 + _2 }
        next if visited.has_key?(candidate)
        next unless cand_height = @heights[candidate]
        next unless cond.(@heights.fetch(current), cand_height)
        visited[candidate] = visited.fetch(current) + 1
        frontier << candidate
      end
      frontier.sort_by { visited.fetch(_1) }
    end
    visited.find {|k,v| to.include?(k) }.last
  end
end
heights = ARGF.read.lines(chomp: true).map(&:chars)
  .flat_map.with_index {|row,y|
    row.map.with_index {|height,x| [[y, x], height] }
  }.to_h
s, e = heights.invert.values_at(?S, ?E)
heights[s] = ?a
heights[e] = ?z
hm = HeightMap.new(heights)
# part one
p hm.shortest(from: s, to: [e]) { _1.ord + 1 >= _2.ord }
# part two
as = heights.select { _2 == ?a }.map(&:first)
p hm.shortest(from: e, to: as) { _1.ord - 1 <= _2.ord }
      -------Part 1--------   -------Part 2--------
Day       Time  Rank  Score       Time  Rank  Score
 13   00:21:02  1075      0   00:23:47   706      0
This one was an exercise in reading comprehension, oof.
def compare(left, right)
  case [left, right]
  in [left, nil]
    1
  in [Integer, Integer]
    left <=> right
  in [Array, Array]
    left.zip(right).each do |left, right|
      case compare(left, right)
      when -1 then return -1
      when 0 # no-op
      when 1 then return 1
      end
    end
    (left.size == right.size) ? 0 : -1
  else
    compare(Array(left), Array(right))
  end
end
# part one
# pairs = ARGF.read.split("\n\n")
# pairs = pairs.map {|pair| pair.lines(chomp: true).map { eval(_1) }}
# p pairs.map.with_index {|(left,right),i|
#   [compare(left, right), i+1]
# }.select {|cmp,_| cmp == -1 }.sum(&:last)
# part two
pairs = ARGF.read.lines(chomp: true).reject(&:empty?).map { eval(_1) }
pairs << [[2]] << [[6]]
pairs = pairs.sort { compare(_1, _2) }
a = pairs.index([[2]])
b = pairs.index([[6]])
p (a+1)*(b+1)
Refactored:
def compare(left, right)
  case [left, right]
  in [left, nil]
    1
  in [Integer, Integer]
    left <=> right
  in [Array, Array]
    left.zip(right).each do |left, right|
      case compare(left, right)
      when -1 then return -1
      when 0 # no-op
      when 1 then return 1
      end
    end
    (left.size == right.size) ? 0 : -1
  else
    compare(Array(left), Array(right))
  end
end
# part one
pairs = ARGF.read.split("\n\n")
pairs = pairs.map {|pair| pair.lines(chomp: true).map { eval(_1) }}
p pairs.map.with_index.select {|(l,r),_| compare(l, r) == -1 }.map { _1.last + 1 }.sum
# part two
pairs = pairs.flatten(1)
dividers = [[[2]], [[6]]]
pairs.concat(dividers)
pairs = pairs.sort { compare(_1, _2) }
p pairs.map.with_index.to_h.values_at(*dividers).map { _1 + 1 }.inject(:*)
      -------Part 1--------   -------Part 2--------
Day       Time  Rank  Score       Time  Rank  Score
 14   00:19:03   560      0   00:23:07   534      0
Brute force.
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
    }
}
x_min, x_max = cave.keys.map(&:first).minmax
y_min, y_max = cave.keys.map(&:last).minmax
inspect_cave = -> {
  puts (0..y_max+1).map {|y|
    (x_min-1..x_max+1).map {|x|
      cave[[x, y]] || ?.
    }.join
  }.join("\n")
}
# part one
# sands = 0
# loop do
#   inspect_cave.()
#   sands += 1
#   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 pos[1] >= y_max
#   end
#   break if pos[1] >= y_max
#   cave[pos] = ?o
# end
# inspect_cave.()
# p sands-1
# part two
cave.default_proc = ->(h,(x,y)) { h[[x, y]] = y == y_max + 2 ? ?# : nil }
sands = 0
loop do
  # inspect_cave.()
  sands += 1
  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
  end
  break if pos == [500, 0]
  cave[pos] = ?o
end
# inspect_cave.()
p sands
I think this must be the least nice code I’ve written so far for Advent of Code.
@alpha It's not so bad - it looks pretty similar to my solution.
Couldn’t help myself and refactored it a bit:
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
      -------Part 1--------   --------Part 2--------
Day       Time  Rank  Score       Time   Rank  Score
 15   00:42:00  2529      0   10:12:53  12339      0
A tough part two - called time on it last night and left an inefficient algorithm run overnight. Came back in the morning and it was only 25% of the way through, but thought of another optimization tweak (jumping to the end of a no-beacon range when the cursor’s x position is farther than the sensor’s x position) that allowed it to finish in just a few minutes.
require "set"
sensors_and_beacons = ARGF.read
  .scan(/Sensor at x=(-?\d+), y=(-?\d+): closest beacon is at x=(-?\d+), y=(-?\d+)/)
  .map { _1.map(&:to_i) }
  .flatten
  .each_slice(2)
# part one
row = 2_000_000
# row = 10
no_beacons = []
sensors_and_beacons.each_slice(2) do |sensor, beacon|
  dist = sensor.zip(beacon).sum { (_1 - _2).abs }
  dy = (row - sensor[1]).abs
  next if dy > dist
  dx = dist - dy
  x_min, x_max = [sensor[0]-dx, sensor[0]+dx].sort
  no_beacons << (x_min..x_max)
end
# remove covered ranges
no_beacons = no_beacons.reject {|x| (no_beacons - [x]).any? { _1.cover?(x) }}
# merge ranges
no_beacons = no_beacons.inject([no_beacons.shift]) {|no_beacons, range|
  next no_beacons if no_beacons.any? { _1.cover?(range) }
  if overlap = no_beacons.find { _1.cover?(range.begin) }
    range = (overlap.end + 1..range.end)
  end
  if overlap = no_beacons.find { _1.cover?(range.end) }
    range = (range.begin..overlap.begin-1)
  end
  no_beacons << range
}
p no_beacons.sum(&:size) - sensors_and_beacons.to_a.select { _2 == row }.uniq.size
# part two
find_sab = ->(x, y) {
  sensors_and_beacons.each_slice(2).find {|sensor, beacon|
    dist = sensor.zip(beacon).sum { (_1 - _2).abs }
    dist >= sensor.zip([x, y]).sum { (_1 - _2).abs }
  }
}
max = 4_000_000
# max = 20
x = 0
y = 0
loop do
  p y if (y % 10_000).zero?
  sensor, _ = find_sab.(x, y)
  dx = sensor[0] - x
  if dx >= max / 2
    dy = sensor[1] - y
    y += dy.positive? ? 2 * dy + 1 : 1
  end
  loop do
    sensor, beacon = find_sab.(x, y)
    if sensor.nil?
      p 4_000_000 * x + y
      exit
    end
    dist = sensor.zip(beacon).sum { (_1 - _2).abs }
    dy = (sensor[1] - y).abs
    dx = (dist - dy).abs
    x = sensor[0] + dx + 1
    if x > max
      x = 0
      break
    end
  end
  y += 1
  break if y > max
end
      -------Part 1--------   --------Part 2--------
Day       Time  Rank  Score       Time   Rank  Score
 18   00:02:28    46     55   01:00:30   2053      0
What a wild swing between part one and part two. I thought my initial approach was inherently wrong on part two, and worked on a few other ideas for a long time before realizing I just needed a .uniq to make it performant.
cubes = ARGF.read.lines(chomp: true).map { _1.split(?,).map(&:to_i) }
# part one
# p cubes.sum {|cube|
#   6 - cubes.count {|other|
#     cube.zip(other).map { _1 - _2 }.map(&:abs).sum == 1
#   }
# }
deltas = [
  [-1,  0,  0],
  [ 1,  0,  0],
  [ 0, -1,  0],
  [ 0,  1,  0],
  [ 0,  0, -1],
  [ 0,  0,  1],
]
bounds = cubes.transpose.map(&:minmax).map {|(min, max)| (min-1..max+1) }
cubes = cubes.to_h { [_1, true] }
corners = [
  [bounds[0].begin, bounds[1].begin, bounds[2].begin],
  [bounds[0].begin, bounds[1].begin, bounds[2].end],
  [bounds[0].begin, bounds[1].end, bounds[2].begin],
  [bounds[0].begin, bounds[1].end, bounds[2].end],
  [bounds[0].end, bounds[1].begin, bounds[2].begin],
  [bounds[0].end, bounds[1].begin, bounds[2].end],
  [bounds[0].end, bounds[1].end, bounds[2].begin],
  [bounds[0].end, bounds[1].end, bounds[2].end],
]
frontier = [corners[0]]
seen = Hash.new
until frontier.empty?
  current = frontier.shift
  neighbors = deltas.map {|delta| current.zip(delta).map { _1 + _2 }}
    .select {|neighbor| bounds.zip(neighbor).all? {|(bound, i)| bound.cover?(i) }}
  seen[current] = neighbors.any? { cubes.has_key?(_1) }
  frontier.concat(neighbors.reject {|neighbor| seen.has_key?(neighbor) || cubes.has_key?(neighbor) })
  frontier.uniq!
end
p cubes.keys.sum {|cube|
  neighbors = deltas.map {|delta| cube.zip(delta).map { _1 + _2 }}
  neighbors.count { seen.has_key?(_1) }
}
@alpha Wow you crushed me on part 1, but I guess I beat you on part 2... I really enjoy seeing your solutions!
@BrianZiman Thanks! That’s what I’m hoping when I post them, but I don’t know how many people actually read them!
      -------Part 1--------   --------Part 2--------
Day       Time  Rank  Score       Time   Rank  Score
 20   00:37:21   845      0   00:38:37    557      0
This one was kind of annoying. Easy algorithm, took almost half an hour to realize that the real data had duplicates numbers in it, when the sample data did not. Ugh.
list = ARGF.read.lines(chomp: true).map(&:to_i)
list = list.map { _1 * 811589153 }
list = list.map.with_index { [_2, _1] }
10.times do
  (0...list.size).each do |i|
    j = list.index {|ii,_| ii == i }
    _, n = list.delete_at(j)
    j += n
    j %= list.size
    list.insert(j, [i, n])
  end
end
list = list.map(&:last)
i = list.index(0)
p [1000, 2000, 3000].sum { list.fetch((i + _1) % list.size) }
@alpha At least one!
      -------Part 1--------   --------Part 2--------
Day       Time  Rank  Score       Time   Rank  Score
 21   00:01:55     7     94   00:39:34    989      0
Hilarious leaderboard rank for part one due to metaprogramming shenanigans. :3
Kind of gross implementation for part two though, I think.
monkeys = ARGF.read.lines(chomp: true).to_h {|line| line.split(": ") }
# part one
# monkeys.each do |name, body|
#   define_method(name) do
#     eval(body)
#   end
# end
# p root
# part two
sub_monkeys = monkeys.delete("root").split(" + ")
monkeys.delete("humn")
monkeys.each do |name, body|
  define_method(name) do
    eval(body)
  end
end
target, unknown = sub_monkeys.partition { eval(_1) rescue nil}.map(&:first)
target = eval(target)
monkeys = monkeys.to_h {|monkey, job|
  a, op, b = job.split(" ")
  [monkey, { op:, a:, b: }]
}
monkeys["humn"] = { op: :humn }
def apply(monkeys, monkey)
  job = monkeys.fetch(monkey)
  case op = job.fetch(:op)
  when nil
    job.fetch(:a).to_i
  when :humn
    :humn
  else
    a = apply(monkeys, job.fetch(:a))
    b = apply(monkeys, job.fetch(:b))
    if a.is_a?(Integer) && b.is_a?(Integer)
      eval([a, op, b].join(" "))
    else
      [job.fetch(:op), a, b]
    end
  end
end
tree =  apply(monkeys, unknown)
def reverse(target, tree)
  loop do
    case tree
    in :humn
      return target
    in [op, Integer, b]
      a = tree.fetch(1)
      case op
      when ?* then target /= a
      when ?+ then target -= a
      when ?- then target = a - target
      else         fail tree.inspect
      end
      tree = b
    in [op, a, Integer]
      b = tree.fetch(2)
      case op
      when ?- then target += b
      when ?/ then target *= b
      when ?* then target /= b
      when ?+ then target -= b
      else         fail tree.inspect
      end
      tree = a
    else
      fail tree.inspect
    end
  end
end
p reverse(target, tree)
@alpha I had a really elegant solution for part 2, with a typo in it, so when it didn't work, I assumed my approach was wrong, and simply evaluated the tree by hand with a calculator. Then I went back and fixed my typo. 🤦♂️
      -------Part 1--------   --------Part 2--------
Day       Time  Rank  Score       Time   Rank  Score
 23   00:36:25   623      0   00:38:28    525      0
grove = {}
ARGF.read.lines(chomp: true).each.with_index do |row, y|
  row.chars.each.with_index do |c, x|
    grove[[y,x]] = c == ?#
  end
end
dirs = [
  [[-1, -1], [-1,  0], [-1,  1]],
  [[ 1, -1], [ 1,  0], [ 1,  1]],
  [[-1, -1], [ 0, -1], [ 1, -1]],
  [[-1,  1], [ 0,  1], [ 1,  1]],
]
(1..).each do |i|
  proposals = grove.select { _2 }
    .to_h {|(y,x),_|
      to = if dirs.flatten(1).none? {|dy,dx| grove[[y+dy, x+dx]] }
             [y, x]
           elsif dir = dirs.find {|adj| adj.none? {|dy,dx| grove[[y+dy, x+dx]] }}
             dy, dx = dir[1]
             [y+dy, x+dx]
           else
             [y, x]
           end
      [[y,x], to]
    }
  proposals.each.with_object(Hash.new {|h,k| h[k] = []}) {|(from, to), conflicts|
    conflicts[to] << from
  }.select { _2.size > 1 }.values.flatten(1).each do |elf|
    proposals[elf] = elf
  end
  if proposals.all? { _1 == _2 }
    puts i
    exit
  end
  proposals.each do |from, to|
    grove[from] = false
    grove[to] = true
  end
  dirs.push(dirs.shift)
end
# y = grove.select { _2 }.keys.map(&:first).minmax
# x = grove.select { _2 }.keys.map(&:last).minmax
# p Range.new(*y).sum {|y| Range.new(*x).count {|x| !grove[[y,x]] }}
Probably should’ve just turned to Z3 immediately for part two. This only took 5-10 minutes just now, so likely would’ve cut my time in half, at least.
require "z3"
monkeys = ARGF.read.lines(chomp: true).to_h {|line| line.split(": ") }
sub_monkeys = monkeys.delete("root").split(" + ")
monkeys.delete("humn")
solver = Z3::Solver.new
solver.assert Z3.Int(sub_monkeys[0]) == Z3.Int(sub_monkeys[1])
monkeys.each do |name, job|
  a, op, b = job.split(" ")
  if op.nil?
    solver.assert Z3.Int(name) == a.to_i
  else
    a = a =~ /^\d+$/ ? a.to_i : Z3.Int(a)
    b = b =~ /^\d+$/ ? b.to_i : Z3.Int(b)
    solver.assert Z3.Int(name) == a.send(op, b)
  end
end
fail unless solver.satisfiable?
p solver.model[Z3.Int("humn")]
      -------Part 1--------   --------Part 2--------
Day       Time  Rank  Score       Time   Rank  Score
 25   00:28:10  1214      0          -      -      -
This is my “fuck it, use a solver” technique because I didn’t want to think hard and logically figure out the algorithm.
places = (0..).lazy.map { 5 ** _1 }
digits = { ?- => -1, ?= => -2 }
sum = ARGF.read.lines(chomp: true)
  .map {|snafu|
    snafu.reverse.chars
      .map { digits.fetch(_1, _1.to_i) }
      .zip(places)
      .map { _1 * _2 }
      .sum
  }.sum
n_places = places.slice_after { 2*_1 >= sum }.take(1).to_a.flatten.reverse
require "z3"
solver = Z3::Solver.new
n_places.each do |place|
  solver.assert Z3.Int(place.to_s) <= 2
  solver.assert Z3.Int(place.to_s) >= -2
end
solver.assert n_places.map { _1 * Z3.Int(_1.to_s) }.sum == sum
fail unless solver.satisfiable?
p n_places
  .map { solver.model[Z3.Int(_1.to_s)].to_i }
  .map { digits.invert.fetch(_1, _1) }
  .join
Probably my worst Advent of Code showing so far, but mostly since I was time-boxing my attempts. Looking at the stats, I spent roughly 30 minutes on average, so that’s not too much time invested. Seems unlikely that I’ll come back and noodle on the ones I gave up on, but who knows.
As usual, written in pretty terse Ruby (unintentionally, no solutions over 100 lines of code), and I mostly avoided doing a ton of refactoring except for some of the earlier days when I wound up with extra time. (Regardless, all original solutions are in git history.)
https://git.kejadlen.dev/alpha/advent-of-code/src/branch/main/2022/ruby
      -------Part 1--------   --------Part 2--------
Day       Time  Rank  Score       Time   Rank  Score
 25   00:28:10  1214      0          -      -      -
 23   00:36:25   623      0   00:38:28    525      0
 21   00:01:55     7     94   00:39:34    989      0
 20   00:37:21   845      0   00:38:37    557      0
 18   00:02:28    46     55   01:00:30   2053      0
 17   00:42:46   636      0          -      -      -
 15   00:42:00  2529      0   10:12:53  12339      0
 14   00:19:03   560      0   00:23:07    534      0
 13   00:21:02  1075      0   00:23:47    706      0
 12   00:20:13  1031      0   00:28:51   1414      0
 11   00:16:07   233      0   00:20:48    167      0
 10   00:28:35  6047      0   00:36:57   3289      0
  9   00:22:45  2529      0   00:26:50   1006      0
  8   00:22:03  3656      0   00:36:40   3129      0
  7   00:21:41   959      0   00:27:39    984      0
  6   00:04:58  1987      0   00:05:25   1413      0
  5   00:07:57   216      0   00:09:52    230      0
  4   00:03:18   452      0   00:03:37    147      0
  3   00:04:06   290      0   00:06:07    158      0
  2   00:11:40  3887      0   00:14:04   2142      0
  1   00:00:57    37     64   00:01:29     24     77
@BrianZiman Didn’t feel like thinking about how to do the 2nd part efficiently!