DEV Community

Discussion on: Advent of Code 2020 Solution Megathread - Day 11: Seating System

Collapse
 
sleeplessbyte profile image
Derk-Jan Karrenbeld

OOP in Ruby; I enjoyed writing this!

require 'benchmark'

class Seat
  def initialize(mark)
    self.mark = mark
    self.next_mark = mark
  end

  def valid?
    mark != 'X'
  end

  def empty?
    mark == 'L'
  end

  def occupied?
    mark == '#'
  end

  def floor?
    mark == '.'
  end

  def invalid?
    mark == 'X'
  end

  def seat?
    occupied? || empty?
  end

  def to_s
    mark
  end

  def prepare_to_dance!
    if occupied?
      self.next_mark = 'L'
    elsif empty?
      self.next_mark = '#'
    end

    self
  end

  def will_it_flip?
    self.next_mark != mark
  end

  def do_the_shuffle!
    self.mark = next_mark
  end

  private

  attr_accessor :mark, :next_mark
end

class SeatingGrid
  def self.from_lines(grid)
    height = grid.length
    width = grid[0].chomp.length

    gridline = grid.map(&:chomp).join('')
    SeatingGrid.new(gridline, width: width, height: height)
  end

  def initialize(gridline, width:, height:)
    self.seats = gridline.chars.map { |x| Seat.new(x) }
    self.width = width
    self.height = height

    self._adjacency = {}
    self._visually_adjacent = {}
    self._visually_at = {}
  end

  def at(x, y)
    return Seat.new('X') if x < 0 || x >= width || y < 0 || y >= height
    self[y * width + x]
  end

  def visual_at(x, y, direction:)
    memoized = _visually_at["#{x}.#{y}"]
    return memoized if memoized

    seat = at(x, y)
    if seat.seat? || seat.invalid?
      _visually_at["#{x}.#{y}"] = seat
      return seat
    end

    visual_at(
      x + direction.first,
      y + direction.last,
      direction: direction
    )
  end

  def [](i)
    seats[i]
  end

  def adjacent(x, y)
    memoized = _adjacency["#{x}.#{y}"]
    return memoized if memoized

    _adjacency["#{x}.#{y}"] = [
      at(x - 1, y - 1), # top left
      at(x    , y - 1), # top
      at(x + 1, y - 1), # top right

      at(x + 1, y    ), # right

      at(x + 1, y + 1), # bottom right
      at(x    , y + 1), # bottom
      at(x - 1, y + 1), # bottom left

      at(x - 1, y    ), # left
  ].select(&:seat?)
  end

  def visual_adjacent(x, y)
    memoized = _visually_adjacent["#{x}.#{y}"]
    return memoized if memoized

    _visually_adjacent["#{x}.#{y}"] = [
      visual_at(x - 1, y - 1, direction: [-1, -1]), # top left
      visual_at(x    , y - 1, direction: [ 0, -1]), # top
      visual_at(x + 1, y - 1, direction: [ 1, -1]), # top right

      visual_at(x + 1, y    , direction: [ 1,  0]), # right

      visual_at(x + 1, y + 1, direction: [ 1,  1]), # bottom right
      visual_at(x    , y + 1, direction: [ 0,  1]), # bottom
      visual_at(x - 1, y + 1, direction: [-1,  1]), # bottom left

      visual_at(x - 1, y    , direction: [-1,  0]), # left
    ].select(&:seat?)
  end

  def prepare_to_dance!(x, y)
    at(x, y).prepare_to_dance!
  end

  def each
    (0..width).each do |x|
      (0..height).each do |y|
        seat = at(x, y)
        yield x, y, seat if seat.seat?
      end
    end
  end

  def visualize
    gridline
      .chars
      .each_slice(width)
      .map(&:join)
      .join("\n")
  end

  def occupancy
    seats.count(&:occupied?)
  end

  def dance!
    seats.each(&:do_the_shuffle!)
  end

  private

  attr_accessor :seats, :width, :height
  attr_accessor :_adjacency, :_visually_adjacent, :_visually_at
end

def simulate(grid, adjacency:, packed_when:, verbose: false)
  rules = {}
  rules[-> (seat) { seat.empty? }]    = -> (adjacents) { !(adjacents.any? { |a| a.occupied? }) }
  rules[-> (seat) { seat.occupied? }] = -> (adjacents) { !(adjacents.count { |a| a.occupied? } < packed_when) }

  iteration = 0
  loop do
    changes = 0
    iteration += 1

    grid.each do |x, y, seat|
      next if seat.floor?

      rule = rules.find { |k, _| k.(seat) }
      next unless rule

      adjacents = adjacency.(grid, x, y)
      next unless rule.last.(adjacents)

      changed = grid.prepare_to_dance!(x, y).will_it_flip?
      changes += changed ? 1 : 0
    end

    grid.dance!

    if verbose
      puts "[#{iteration}] #{changes} changes"
      puts
      puts grid.visualize
      puts ""
      puts ""
    end

    if changes.zero?
      puts "==============================" if verbose
      puts "Seats taken: #{grid.occupancy}"
      break
    end
  end
end

source = 'input.txt'

Benchmark.bmbm do |x|
  x.report('part 1') do
    grid = SeatingGrid.from_lines(File.readlines(source))

    simulate(
      grid,
      adjacency: -> (grid, x, y) { grid.adjacent(x, y) },
      packed_when: 4,
    )
  end

  x.report('part 2') do
    grid = SeatingGrid.from_lines(File.readlines(source))

    simulate(
      grid,
      adjacency: -> (grid, x, y) { grid.visual_adjacent(x, y) },
      packed_when: 5
    )
  end
end
Enter fullscreen mode Exit fullscreen mode