class Column attr_reader :floors, :items, :elevator, :history def self.for(lines) floors = lines.each_with_index.map { |line, index| Floor.for line, index + 1 } items = floors.map(&:items).flatten elevator = 1 new floors, items, elevator end def initialize(floors, items, elevator, history=[]) @floors = floors @items = items @elevator = elevator @history = history end def to_s @to_s ||= floors.reverse.map do |floor| floor_num = "F#{floor.number}" elevator_indicator = elevator == floor.number ? "E " : ". " item_string = items.map do |item| floor.has_item?(item) ? item.abbr : ". " end.join(" ") "#{floor_num} #{elevator_indicator} #{item_string}" end.join("\n") end def valid? floors.all? &:valid? end def repeat? history.include? to_s end def apply(move) from_floor = floors.find { |floor| move.from == floor.number } to_floor = floors.find { |floor| move.to == floor.number } other_floors = floors.find_all { |floor| ![move.from, move.to].include?(floor.number) } new_from_floor = Floor.new(from_floor.items - move.with, from_floor.number) new_to_floor = Floor.new(to_floor.items + move.with, to_floor.number) self.class.new [new_from_floor, new_to_floor, *other_floors].sort_by(&:number), items, move.to, self.history + [to_s] end def complete? (items - floors.last.items).empty? end def current_floor floors.find { |floor| elevator == floor.number } end def generate_moves current_floor.generate_moves end def next_states generate_moves.map { |move| apply(move) }.select(&:valid?).reject(&:repeat?) end end class Floor attr_reader :items, :number def initialize(items, number) @items = items @number = number end def self.for(line, number) strings = line.gsub(" and", "").gsub(",", "").gsub(".", "").split(" a ")[1..] new strings.map { |string| Item.for string }, number end def has_item?(item) items.include? item end def valid? items.all? { |item| item.valid_on? self } end def generators items.select &:generator? end def microchips items.select &:microchip? end def has_matching_generator?(element) generators.any? { |gen| gen.is_element? element } end def generate_moves [*one_item_moves, *two_item_moves] end def one_item_moves [*one_item_up_moves, *one_item_down_moves] end def one_item_up_moves return [] if number == 4 items.map do |item| Move.new number, number + 1, [item] end end def one_item_down_moves return [] if number == 1 items.map do |item| Move.new number, number - 1, [item] end end def two_item_moves [*two_item_up_moves, *two_item_down_moves] end def two_item_up_moves return [] if number == 4 items.each_with_index.map do |item_one, i1| items.each_with_index.map do |item_two, i2| next if item_one == item_two next if i1 < i2 Move.new number, number + 1, [item_one, item_two] end end.flatten.compact end def two_item_down_moves return [] if number == 1 items.each_with_index.map do |item_one, i1| items.each_with_index.map do |item_two, i2| next if item_one == item_two next if i1 < i2 Move.new number, number - 1, [item_one, item_two] end end.flatten.compact end end class Item attr_reader :element def initialize(element) @element = element end def self.for(string) case string.split[1] when "microchip" Microchip.new string.split[0].split("-")[0] when "generator" Generator.new string.split[0] end end def element_abbr element[0..1].upcase end def abbr "#{element_abbr}#{object_abbr}" end def generator? false end def microchip? false end def is_element?(other_element) element == other_element end end class Microchip < Item def object_abbr "M" end def valid_on?(floor) floor.has_matching_generator?(element) || floor.generators.empty? end def microchip? true end def to_s @to_s ||= "#{element} microchip" end end class Generator < Item def object_abbr "G" end def valid_on?(_) true end def generator? true end def to_s @to_s ||= "#{element} generator" end end class Move attr_reader :from, :to, :with def initialize(from, to, with) @from = from @to = to @with = with end def to_s "From #{from} to #{to} with #{with.map(&:to_s)}" end end input = STDIN.read.chomp lines = input.split("\n") column = Column.for(lines) states = [column] until states.any?(&:complete?) states = states.map(&:next_states).flatten.uniq(&:to_s) puts "next" end part_1 = states.find(&:complete?).history.count puts "Part 1: #{part_1}"