Advent of Code 2020: Day 11 (Ruby solution)

This time the tasks were about modeling people taking seats in the waiting area. The seat layout is given as an input file.

The process of taking seats is iterative. Rules which are applied to every seat simultaneously differ for the first and second tasks.

In the first puzzle:

  • If a seat is empty (L) and there are no occupied seats adjacent to it, the seat becomes occupied.
  • If a seat is occupied (#) and four or more seats adjacent to it are also occupied, the seat becomes empty.
  • Otherwise, the seat's state does not change.
  • Floor (.) never changes; seats don't move, and nobody sits on the floor.

In the second one, instead of considering just the eight immediately adjacent seats, consider the first seat in each of the eight directions. It now takes five or more visible occupied seats for an occupied seat to become empty. The rest of the rules stay unchanged.

To deal with those tasks, I implemented WaitingArea class and two different taking seat policies.

require 'matrix'

class WaitingArea
  def initialize file
    @seats = Matrix.rows file.lines.map { |line| line.strip.chars }
  end

  def take_seats policy_class
    policy = policy_class.new(@seats)
    new_seats = @seats.dup
    @seats.each_with_index do |e, row, col|
      next if e == '.'
      new_seats[row, col] = policy.take_seat?(row, col) ? '#' : 'L'
    end
    @seats = new_seats
  end

  def occupied_seats
    @seats.select { |adjacent_seat| adjacent_seat == '#' }.size
  end
end

class SimpleTakingSeatPolicy
  def initialize seats
    @seats = seats
  end

  def take_seat? row, col
    seat = @seats[row, col]
    adjacent_seats = @seats.minor(
        [row - 1, 0].max..[row + 1, @seats.row_count - 1].min,
        [col - 1, 0].max..[col + 1, @seats.column_count - 1].min
    )
    occupied_adjacent_seats = adjacent_seats.select { |adjacent_seat| adjacent_seat == '#' }.size
    return occupied_adjacent_seats <= 4 if seat == '#'
    occupied_adjacent_seats.zero?
  end
end

class AlternativeTakingSeatPolicy
  def initialize seats
    @seats = seats
  end

  def take_seat? row, col
    seat = @seats[row, col]
    occupied_visible_seats = 0
    ([-1, 0, +1].repeated_permutation(2).to_a - [[0, 0]]).each do |row_mod, col_mod|
      i = 0
      while i += 1
        x = row + (row_mod * i)
        y = col + (col_mod * i)
        break if x < 0 || x >= @seats.row_count
        break if y < 0 || y >= @seats.column_count
        tmp_seat = @seats[x, y]
        next if tmp_seat == '.'
        occupied_visible_seats += 1 if tmp_seat == '#'
        break
      end
    end
    return occupied_visible_seats < 5 if seat == '#'
    occupied_visible_seats.zero?
  end
end

file = File.read('inputs/day11.txt')
[SimpleTakingSeatPolicy, AlternativeTakingSeatPolicy].each_with_index do |policy, index|
  craft = WaitingArea.new(file)
  occupied_seats = nil
  while occupied_seats != occupied_seats = craft.occupied_seats
    craft.take_seats(policy)
  end
  puts craft.occupied_seats
end