Advent of Code 2022, Day 23: Unstable Diffusion

#ruby #advent of code 2022

Part A

On Day 23 we need to simulate the movements of multiple elves on the map. Our input looks like this:

....#..
..###.#
#...#.#
.#...##
#.###..
##.#.##
.#..#..

Each dot represents empty space and hash represents some elf current position. Elves can move according to the list of rules that are described on the puzzle page.

This task is not that challanging, you just need to implement rules correctly:

class Elf
  attr_accessor :x, :y, :moves

  def initialize(x, y, map)
    @x = x
    @y = y
    @moves = [:north, :south, :west, :east]
    @map = map
  end

  def moves
    @moves
  end

  def move(nx, ny)
    set(@x, @y, false)
    set(nx, ny, true)

    @x = nx
    @y = ny
  end

  def can_move?(direction)
    case direction
    when :north
      [get(@x - 1, @y - 1), get(@x, @y - 1), get(@x + 1, @y - 1)] == [false, false, false]
    when :south
      [get(@x - 1, @y + 1), get(@x, @y + 1), get(@x + 1, @y + 1)] == [false, false, false]
    when :west
      [get(@x - 1, @y - 1), get(@x - 1, @y), get(@x - 1, @y + 1)] == [false, false, false]
    when :east
      [get(@x + 1, @y - 1), get(@x + 1, @y), get(@x + 1, @y + 1)] == [false, false, false]
    end
  end

  def new_position(direction)
    case direction
    when :north
      [@x, @y - 1]
    when :south
      [@x, @y + 1]
    when :west
      [@x - 1, @y]
    when :east
      [@x + 1, @y]
    end
  end

  def elves_nearby?
    [
      [@x - 1, @y - 1],
      [@x, @y - 1],
      [@x + 1, @y - 1],
      [@x - 1, @y],
      [@x + 1, @y],
      [@x - 1, @y + 1],
      [@x, @y + 1],
      [@x + 1, @y + 1]
    ].any? { |(x, y)| get(x, y) }
  end
end

def get(x, y)
  !!@map[[x, y]]
end

def set(x, y, value)
  @map[[x, y]] = value
end

def draw_map
  min_x, max_x = @elves.map { |elf| elf.x }.minmax
  min_y, max_y = @elves.map { |elf| elf.y }.minmax

  [0, min_y].min.upto([@map_y - 1, max_y].max) do |y|
    row = ""
    [0, min_x].min.upto([max_x, @map_x - 1].max) do |x|
      row += get(x, y) ? "#" : "."
    end
    puts row
  end
end

def area
  min_x, max_x = @elves.map { |elf| elf.x }.minmax
  min_y, max_y = @elves.map { |elf| elf.y }.minmax

  result = 0

  min_y.upto(max_y) do |y|
    min_x.upto(max_x) do |x|
      result += 1 unless get(x, y)
    end
  end

  result
end

data = File.readlines("23.txt", chomp: true).map { |row| row.split("") }
@elves = []
@map = {}
@map_x = data[0].size
@map_y = data.size

data.each_with_index do |row, y|
  row.each_with_index do |tile, x|
    if tile == "#"
      @elves.push(Elf.new(x, y, @map))
      set(x, y, true)
    end
  end
end

10.times do |round|
  proposals = {}

  @elves.each_with_index do |elf, index|
    if elf.elves_nearby?
      direction = elf.moves.detect { |direction| elf.can_move?(direction) }
      new_position = elf.new_position(direction)

      proposals[new_position] ||= []
      proposals[new_position].push(elf)
    end

    elf.moves.rotate!
  end

  proposals.each do |(x, y), elves|
    if elves.size == 1
      elves[0].move(x, y)
    end
  end
end

puts area

Part B

This part is very similar. In first part we need to simulate 10 rounds and in second part we need to simulate until all elves are spread out.

Here is my full code:

class Elf
  attr_accessor :x, :y, :moves

  def initialize(x, y, map)
    @x = x
    @y = y
    @moves = [:north, :south, :west, :east]
    @map = map
  end

  def moves
    @moves
  end

  def move(nx, ny)
    set(@x, @y, false)
    set(nx, ny, true)

    @x = nx
    @y = ny
  end

  def can_move?(direction)
    case direction
    when :north
      [get(@x - 1, @y - 1), get(@x, @y - 1), get(@x + 1, @y - 1)] == [false, false, false]
    when :south
      [get(@x - 1, @y + 1), get(@x, @y + 1), get(@x + 1, @y + 1)] == [false, false, false]
    when :west
      [get(@x - 1, @y - 1), get(@x - 1, @y), get(@x - 1, @y + 1)] == [false, false, false]
    when :east
      [get(@x + 1, @y - 1), get(@x + 1, @y), get(@x + 1, @y + 1)] == [false, false, false]
    end
  end

  def new_position(direction)
    case direction
    when :north
      [@x, @y - 1]
    when :south
      [@x, @y + 1]
    when :west
      [@x - 1, @y]
    when :east
      [@x + 1, @y]
    end
  end

  def elves_nearby?
    [
      [@x - 1, @y - 1],
      [@x, @y - 1],
      [@x + 1, @y - 1],
      [@x - 1, @y],
      [@x + 1, @y],
      [@x - 1, @y + 1],
      [@x, @y + 1],
      [@x + 1, @y + 1]
    ].any? { |(x, y)| get(x, y) }
  end
end

def get(x, y)
  !!@map[[x, y]]
end

def set(x, y, value)
  @map[[x, y]] = value
end

def draw_map
  min_x, max_x = @elves.map { |elf| elf.x }.minmax
  min_y, max_y = @elves.map { |elf| elf.y }.minmax

  [0, min_y].min.upto([@map_y - 1, max_y].max) do |y|
    row = ""
    [0, min_x].min.upto([max_x, @map_x - 1].max) do |x|
      row += get(x, y) ? "#" : "."
    end
    puts row
  end
end

def area
  min_x, max_x = @elves.map { |elf| elf.x }.minmax
  min_y, max_y = @elves.map { |elf| elf.y }.minmax

  result = 0

  min_y.upto(max_y) do |y|
    min_x.upto(max_x) do |x|
      result += 1 unless get(x, y)
    end
  end

  result
end

data = File.readlines("23.txt", chomp: true).map { |row| row.split("") }
@elves = []
@map = {}
@map_x = data[0].size
@map_y = data.size

data.each_with_index do |row, y|
  row.each_with_index do |tile, x|
    if tile == "#"
      @elves.push(Elf.new(x, y, @map))
      set(x, y, true)
    end
  end
end

round = 0

while true
  proposals = {}
  elves_skipped = 0

  @elves.each_with_index do |elf, index|
    if elf.elves_nearby?
      direction = elf.moves.detect { |direction| elf.can_move?(direction) }

      if direction
        new_position = elf.new_position(direction)

        proposals[new_position] ||= []
        proposals[new_position].push(elf)
      else
        elves_skipped += 1
      end
    else
      elves_skipped += 1
    end

    elf.moves.rotate!
  end

  if elves_skipped == @elves.size
    puts round + 1
    break
  end

  proposals.each do |(x, y), elves|
    if elves.size == 1
      elves[0].move(x, y)
    end
  end

  round += 1
end

I also included code for drawing the current map so it is possible to see how elves are positioned at each round.