יום 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 וכך למנוע מצב של הערות מבלבלות או לא מתוחזקות. אשמח לשמוע גם מה דעתכם.