diff --git a/2019/day17/part2.rb b/2019/day17/part2.rb new file mode 100644 index 0000000..5578c72 --- /dev/null +++ b/2019/day17/part2.rb @@ -0,0 +1,325 @@ +# frozen_string_literal: true + +require_relative 'intcode' + +Cell = Struct.new(:r, :c) do + def neighbors + [ + Cell.new(r + 1, c), + Cell.new(r - 1, c), + Cell.new(r, c + 1), + Cell.new(r, c - 1) + ] + end + + def neighbor(dir) + case dir + when :up + Cell.new(r - 1, c) + when :down + Cell.new(r + 1, c) + when :left + Cell.new(r, c - 1) + when :right + Cell.new(r, c + 1) + end + end + + def can_move?(dir, scaffold) + scaffold.include? neighbor(dir) + end + + def move(dir) + neighbor dir + end +end + +State = Struct.new(:pos, :dir) + +def bfs(start, get_neighbors, is_goal) + queue = [[start]] + visited = Set.new + until queue.empty? + path = queue.shift + state = path[-1] + next if visited.include? state + + visited << state + return path if is_goal.call(state) + + children = get_neighbors.call(state).reject { |cell| visited.include? cell } + children.each do |child| + queue << path + [child] + end + end + nil +end + +def turn(old, new) + dirs = %i[up right down left] + i = dirs.index old + j = dirs.index new + + lefts = ((i - j) % 4).abs + rights = ((j - i) % 4).abs + if lefts < rights + ['L'] * lefts + else + ['R'] * rights + end +end + +def valid_turns(dir) + case dir + when :up, :down + %i[left right] + when :left, :right + %i[up down] + end +end + +def follow_path(start_cell, start_dir, scaffold) + path = [] + cell = start_cell + dir = start_dir + loop do + while cell.can_move?(dir, scaffold) + cell = cell.move dir + path << 1 + end + old_dir = dir + dir = valid_turns(old_dir).find do |d| + cell.can_move?(d, scaffold) + end + break if dir.nil? + + path.concat turn(old_dir, dir) + end + path +end + +def condense(path) + condense_forward(path).each_slice(2) +end + +def condense_forward(path) + new_path = [0] + path.each do |move| + both_numeric = move.is_a?(Numeric) && new_path.last.is_a?(Numeric) + new_path << (both_numeric ? new_path.pop + move : move) + end + new_path.shift if new_path.first.zero? + new_path +end + +def create_functions(actions) + valid = proc { |p| p.join(',').size < 20 } + (1..5).reverse.each do |a_length| + actions.uniq.repeated_permutation(a_length).select(&valid).each do |_a| + (1..5).reverse.each do |b_length| + actions.uniq.repeated_permutation(b_length).select(&valid).each do |_b| + (1..5).reverse.each do |c_length| + actions.uniq.repeated_permutation(c_length).select(&valid).each do |_c| + Plan.new + end + end + end + end + end + end +end + +Plan = Struct.new(:main, :a, :b, :c) do + def expand + main + .gsub(/A/, a) + .gsub(/B/, b) + .gsub(/C/, c) + end + + def validate! + errors = [] + + errors << 'Main is too long' unless main.size < 20 + errors << 'A is too long' unless a.size < 20 + errors << 'B is too long' unless b.size < 20 + errors << 'C is too long' unless c.size < 20 + errors << 'Main has invalid characters' unless main.chars.all? { |c| ['A', 'B', 'C', ','].include? c } + + errors.each { |error| puts error } + raise StandardError, 'Plan failed to validate!' unless errors.empty? + end +end + +MEMORY_LIMIT = 20 # characters + +class World + def initialize + @cells = {} + @max_r = -1 + @max_c = -1 + end + + def set(cell, contents) + @cells[cell] = contents + @max_r = [@max_r, cell.r].max + @max_c = [@max_c, cell.c].max + end + + ROBOTS = %i[up down left right].freeze + + def plan + # From my input, I know that my robot starts on one end of the scaffolding. + # Also from my input, I know that following the path directly (continuing + # "forward" at every intersection) gets the robot to the end. I'm going to + # hope that I can write a program that takes every loop the "right" way + # first. + start = @cells.find do |_cell, contents| + ROBOTS.include? contents + end + start_cell, start_dir = start + + scaffold = @cells.keys.select { |cell| scaffold?(cell) } + path = follow_path(start_cell, start_dir, scaffold) + condense(path) + end + + def intersections + @cells.keys.select { |cell| intersection? cell } + end + + def intersection?(cell) + scaffold?(cell) && cell.neighbors.all? { |neighbor| scaffold?(neighbor) } + end + + SCAFFOLDS = %i[scaffold up down left right].freeze + + def scaffold?(cell) + SCAFFOLDS.include? @cells[cell] + end + + def to_s + s = String.new + (0..@max_r).each do |r| + (0..@max_c).each do |c| + s << char(@cells[Cell.new(r, c)]) + end + s << "\n" + end + s + end + + CHAR_TO_CONTENTS = { + '#' => :scaffold, + '.' => :open, + '^' => :up, + 'v' => :down, + '<' => :left, + '>' => :right, + 'x' => :tumbling + }.freeze + + CONTENTS_TO_CHAR = { + scaffold: '#', + open: '.', + up: '^', + down: 'v', + left: '<', + right: '>', + tumbling: 'x' + }.freeze + + def self.parse(text) + r = c = 0 + world = new + text.each_char do |char| + if char == "\n" + r += 1 + c = 0 + next + end + contents = CHAR_TO_CONTENTS.fetch(char) + world.set(Cell.new(r, c), contents) + c += 1 + end + world + end + + private + + def char(contents) + CONTENTS_TO_CHAR.fetch(contents) + end +end + +class Calibrator + attr_reader :world + + def initialize(program) + @computer = Intcode::Computer.new(program.as_memory) + @world = nil + end + + def calibrate + text = '' + while (i = tick) + text += i.chr + end + @world = World.parse(text) + alignment(@world.intersections) + end + + def alignment(intersections) + intersections.sum do |cell| + cell.r * cell.c + end + end + + def tick + @computer.next_output + end +end + +class Robot + def initialize(program, input_queue) + @computer = Intcode::Computer.new(program.as_memory) { on_input } + @input_queue = input_queue.clone + end + + def on_input + @input_queue.shift + end + + def tick + @computer.next_output + end +end + +main = 'A,B,A,C,B,C,A,C,B,C' +a = 'L,8,R,10,L,10' +b = 'R,10,L,8,L,8,L,10' +c = 'L,4,L,6,L,8,L,8' +plan = Plan.new(main, a, b, c) + +plan.validate! + +def to_input(function) + function.chars.map(&:ord) + ["\n".ord] +end + +video_feed = 'n' +input_queue = [plan.main, plan.a, plan.b, plan.c].flat_map { |fn| to_input(fn) } +input_queue.concat "#{video_feed}\n".chars.map(&:ord) + +program = Intcode.read(File.join(__dir__, 'input2')) +robot = Robot.new(program, input_queue) + +outputs = [] +while (out = robot.tick) + outputs << out +end + +outputs[0...-1].each do |out| + print out.chr +end +puts outputs.last