• בלוג
  • פתרון AOC 2025 יום 4 (ומה קרה להערות)

פתרון AOC 2025 יום 4 (ומה קרה להערות)

12/12/2025

יום 4 של Advent Of Code הוא בסך הכל עוד גרסה של משחק החיים. אנחנו מקבלים קלט במטריצה שנראית בערך כך:

..@@.@@@@.
@@@.@.@.@@
@@@@@.@.@@
@.@@@@..@.
@@.@@@@.@@
.@@@@@@@.@
.@.@.@.@@@
@.@@@.@@@@
.@@@@@@@@.
@.@.@@@.@.

וצריכים למצוא לכמה שטרודלים יש פחות מ-4 שכנים, ואז בחלק השני למחוק את כל אלה שיש להם פחות מ-4 שכנים ולהמשיך בלולאה עד שנתקעים. ככה זה נראה ברובי:

class Day4
  attr_accessor :map

  def initialize(fname)
    @map = {}

    File.read(fname).lines.each_with_index do |line, row|
      line.strip.chars.each_with_index do |char, column|
        map[[row, column]] = char
      end
    end
  end

  def part1
    map
      .filter {|k, v| v == "@" }
      .map {|k, v| [k, count_taken_neighbors(k), count_taken_neighbors(k) < 4 ] }
      .count {|(_, _, c)| c }
  end

  def part2
    removed = 0

    loop do
      next_remove_round = removable.keys
      break if next_remove_round.empty?

      removed += next_remove_round.size
      self.map = map.except(*next_remove_round)
    end

    removed
  end

  def removable
    map
      .filter {|k, v| v == "@" && count_taken_neighbors(k) < 4 }
  end

  def count_taken_neighbors((row, column))
    map.values_at(*neighbor_indexes(row, column)).filter {|v| v == "@" }.count
  end

  def neighbor_indexes(row, column)
    [
      [row-1, column-1],
      [row-1, column],
      [row-1, column+1],
      [row, column-1],
      [row, column+1],
      [row+1, column-1],
      [row+1, column],
      [row+1, column+1],
    ]
  end
end

if __FILE__ == $PROGRAM_NAME
  d = Day4.new('input.txt')
  pp d.part1
  pp d.part2
end

זה לא היה מסובך ואני גם חושב שלא בזבזני מדי. נכון בחלק השני אפשר היה להיות יותר יעילים, לשמור אינדקס ולעדכן רק את השכנים אחרי כל מחיקה אבל באמת שאין פה צורך בזה והכל עובד מהר גם על קלט גדול.

עכשיו לשאלה השניה מהכותרת - מה עם הערות? כל אחד חושב שהקוד שלו הוא שירה אבל האמת שרובכם בטח קוראים את הרובי הזה ולא מבינים מה רציתי מכם, מה זה הכוכבית בשם הפרמטר ל except, מה ההבדל בין size ל count, מה כל הקווים האנכיים ומהם המקרים הלא מטופלים. בעולם הישן הייתם יכולים להמשיך לחלום על תיעוד כי ממילא אף אחד לא כתב אותו. היום אפשר להדביק את הסקריפט הזה ל ChatGPT ולקבל את כל התמונה. זה מה שיוצא:

    # Day4
    #
    # This class loads a 2D grid of characters from an input file and performs two
    # computations ("part1" and "part2") on that grid. The grid is stored in a hash
    # mapping `[row, column]` coordinate pairs to single-character strings.
    #
    # The characters of interest are '@', which represent "taken" cells. The script
    # is effectively analyzing a grid of occupied/unoccupied positions and applying
    # neighbor-based rules to count or remove cells.
    #
    class Day4
      # @!attribute [rw] map
      #   @return [Hash{Array(Integer,Integer) => String}]
      #   A hash where keys are [row, column] coordinate pairs and values are
      #   characters from the input grid. This is the central data structure on
      #   which all computations operate.
      attr_accessor :map

      # Create a new grid from a filename.
      #
      # @param fname [String] path to the input file
      #
      # Reads the file line by line, strips whitespace, and inserts each character
      # into the `map` hash keyed by its (row, column) position. This establishes
      # the full input state for later analysis.
      def initialize(fname)
        @map = {}

        File.read(fname).lines.each_with_index do |line, row|
          line.strip.chars.each_with_index do |char, column|
            map[[row, column]] = char
          end
        end
      end

      # Compute Part 1's result.
      #
      # Part 1 counts how many '@' cells have **fewer than four '@' neighbors**.
      # The logic:
      #   1. Filter to only "taken" cells (value == '@')
      #   2. For each such cell, compute the number of '@' neighbors
      #   3. Count how many of those have < 4 neighbors
      #
      # @return [Integer] number of '@' cells with fewer than four '@' neighbors
      def part1
        map
          .filter { |_, v| v == "@" }
          .map { |k, v| [k, count_taken_neighbors(k), count_taken_neighbors(k) < 4] }
          .count { |(_, _, needs_removal)| needs_removal }
      end

      # Compute Part 2's result.
      #
      # Part 2 repeatedly removes removable '@' cells until none remain.
      # A cell is removable if it has fewer than four '@' neighbors (via #removable).
      #
      # This simulates a stabilizing pruning process: at each round, all currently
      # removable '@' cells are removed simultaneously. The process continues until
      # no '@' cell qualifies for removal. The method returns the number of cells
      # removed across all rounds.
      #
      # @return [Integer] total number of removed '@' cells
      def part2
        removed = 0

        loop do
          next_remove_round = removable.keys
          break if next_remove_round.empty?

          removed += next_remove_round.size
          self.map = map.except(*next_remove_round)
        end

        removed
      end

      # Determine which '@' cells are currently removable.
      #
      # A cell is removable if:
      #   - Its value is '@'
      #   - It has fewer than four '@' neighbors
      #
      # Used both by part1 (conceptually identical criteria) and part2 (for iterative
      # removal rounds).
      #
      # @return [Hash] subset of `map` containing only removable '@' entries
      def removable
        map
          .filter { |k, v| v == "@" && count_taken_neighbors(k) < 4 }
      end

      # Count how many neighbors of a given cell are '@'.
      #
      # @param row_column [Array(Integer,Integer)] the (row, column) location
      #
      # Uses #neighbor_indexes to obtain adjacent coordinates in an 8-connected
      # neighborhood, then counts the ones whose value in the map is '@'.
      #
      # @return [Integer] the number of occupied ('@') neighbors
      def count_taken_neighbors((row, column))
        map
          .values_at(*neighbor_indexes(row, column))
          .count { |v| v == "@" }
      end

      # Compute the 8 neighboring coordinates around (row, column).
      #
      # This includes diagonals:
      #
      #   (-1,-1) (-1,0) (-1,+1)
      #   ( 0,-1)   X    ( 0,+1)
      #   (+1,-1) (+1,0) (+1,+1)
      #
      # The returned coordinates may or may not exist in the map; callers handle
      # missing keys implicitly via Hash#values_at returning nil.
      #
      # @return [Array<Array(Integer,Integer)>] list of neighbor coordinate pairs
      def neighbor_indexes(row, column)
        [
          [row - 1, column - 1],
          [row - 1, column],
          [row - 1, column + 1],
          [row,     column - 1],
          [row,     column + 1],
          [row + 1, column - 1],
          [row + 1, column],
          [row + 1, column + 1],
        ]
      end
    end

    # If this file is run directly, load the puzzle input and print Part 1 and Part 2 solutions.
    if __FILE__ == $PROGRAM_NAME
      d = Day4.new('input.txt')
      pp d.part1
      pp d.part2
    end

נסו עכשיו לקרוא את ההסברים של ChatGPT ותראו אם הקוד יוצא יותר ברור. לדעתי כן. ההתלבטות שעדיין נשארה כשמסתכלים (וכותבים) מערכות, היא אם ChatGPT מסוגל לכתוב תיעוד מדויק על כל קוד האם שווה לי לשמור את התיעוד הזה בגוף הקוד כחלק מקבצי המקור או שהגיוני יותר לייצר את התיעוד On Demand. אני נוטה לכיוון הייצור On Demand וכך למנוע מצב של הערות מבלבלות או לא מתוחזקות. אשמח לשמוע גם מה דעתכם.