• בלוג
  • ניסוי Advent Of Code יום 8 וקלוד

ניסוי Advent Of Code יום 8 וקלוד

17/04/2025

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

1. האתגר

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

............
........0...
.....0......
.......0....
....0.......
......A.....
............
............
........A...
.........A..
............
............

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

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

..........
...#......
..........
....a.....
..........
.....a....
..........
......#...
..........
..........

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

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

הפיתרון שלי לתרגיל בשפת רובי הוא:

# frozen_string_literal: true

Point = Data.define(:x, :y) do
  def manhattan_distance(other)
    [(x - other.x).abs, (y - other.y).abs]
  end

  def find_antinodes(other)
    diff_x, diff_y = manhattan_distance(other)
    if x < other.x
      if y < other.y
        # p1 is to the left and above p2
        antinode1 = Point.new(x - diff_x, y - diff_y)
        antinode2 = Point.new(other.x + diff_x, other.y + diff_y)
      else
        # p1 is to the left and below p2
        antinode1 = Point.new(x - diff_x, y + diff_y)
        antinode2 = Point.new(other.x + diff_x, other.y - diff_y)
      end
    else
      if y < other.y
        # p1 is to the right and above p2
        antinode1 = Point.new(x + diff_x, y - diff_y)
        antinode2 = Point.new(other.x - diff_x, other.y + diff_y)
      else
        # p1 is to the right and below p2
        antinode1 = Point.new(x + diff_x, y + diff_y)
        antinode2 = Point.new(other.x - diff_x, other.y - diff_y)
      end
    end

    [antinode1, antinode2]
  end
end

class Map
  attr_accessor :matrix, :index_by_letter, :max_x, :may_y
  def initialize(input_file_name)
    @index_by_letter = Hash.new { |h, k| h[k] = [] }
    @max_x = 0
    @max_y = 0
    File.read(input_file_name).lines.each_with_index.flat_map do |line, line_index|
      line.chomp.split('').each_with_index.map do |char, column_index|
        pos = Point.new(column_index, line_index) 
        @max_y = line_index if line_index > @max_y
        @max_x = column_index if column_index > @max_x
        @index_by_letter[char] << pos if char != '.'
      end
    end
  end

  def contains(p)
    p.x <= @max_x && p.x >= 0 && p.y <= @max_y && p.y >= 0
  end
end

m = Map.new('input.txt')

antinodes = m.index_by_letter.flat_map do |_char, positions|
  positions.combination(2).flat_map { |p1, p2| p1.find_antinodes(p2) }
end


puts antinodes.filter { |p| m.contains(p) }.uniq.count

עכשיו לניסוי עם קלוד.

2. ניסיון ראשון - רק הבעיה

התחלתי את הניסוי עם הפרומפט הבא:

Solve the following general problem in Ruby (input is saved in file input.txt)

אחריו הדבקתי פשוט את כל עמוד התיאור של הבעיה מ Advent Of Code: https://adventofcode.com/2024/day/8

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

3. ניסוי שני - דחיפה קלה עם API Docs

המשכתי את הבדיקה עם פרומפט יותר ארוך. הפעם נתתי לו תיעוד API Docs של שתי המחלקות שכתבתי (קלוד עצמו יצר את התיעוד בשיחה אחרת איתו) ואז את הבעיה. אני מזכיר שבפיתרון שלי אחרי שכתבתי את הקוד לשתי המחלקות הפיתרון עצמו לקח בערך 5 שורות. כמו שחשדתי קלוד הסתבך בעיקר עם הבנת השאלה אבל תיעוד ה API כן עזר לו לחשוב והפעם הוא הדפיס קוד שהיה מרוצה ממנו אחרי 4 פעמים בלבד.

החיסרון? הקוד לא עבד והדפיס 0. זה מה שקלוד הדפיס:

Point = Data.define(:x, :y) do
  def manhattan_distance(other)
    [
      (self.x - other.x).abs,
      (self.y - other.y).abs
    ]
  end

  def find_antinodes(other)
    # Calculate the differences between the two points
    dx = other.x - self.x
    dy = other.y - self.y

    # Create antinode points
    antinode1 = Point.new(self.x - dx, self.y - dy)
    antinode2 = Point.new(other.x + dx, other.y + dy)

    [antinode1, antinode2]
  end
end

class Map
  attr_reader :index_by_letter, :max_x, :max_y

  def initialize(input_file_name)
    @index_by_letter = Hash.new { |h, k| h[k] = [] }
    @max_x = 0
    @max_y = 0

    File.readlines(input_file_name).each_with_index do |line, y|
      line = line.chomp
      @max_x = [line.length - 1, @max_x].max
      @max_y = [y, @max_y].max

      line.chars.each_with_index do |char, x|
        unless char == '.'
          @index_by_letter[char] << Point.new(x, y)
        end
      end
    end
  end

  def contains(p)
    p.x >= 0 && p.x <= @max_x && p.y >= 0 && p.y <= @max_y
  end
end

def find_antinodes(map)
  antinodes = Set.new

  # Process each frequency (letter)
  map.index_by_letter.each do |letter, positions|
    # We need at least 2 antennas with the same frequency
    next if positions.length < 2

    # Check all pairs of antennas with the same frequency
    positions.combination(2).each do |p1, p2|
      # Calculate Manhattan distance components
      diff_x, diff_y = p1.manhattan_distance(p2)

      # Skip if the points aren't aligned (Manhattan distance check)
      next unless diff_x == 0 || diff_y == 0 || diff_x == diff_y

      # Calculate total Manhattan distance
      total_distance = diff_x + diff_y

      # Calculate distance between p1 and midpoint
      mid_x = (p1.x + p2.x) / 2.0
      mid_y = (p1.y + p2.y) / 2.0
      dist_to_mid = (p1.x - mid_x).abs + (p1.y - mid_y).abs

      # Check if one antenna is twice as far from the midpoint as the other
      # This is equivalent to checking if the ratio of distances is 1:2
      next unless (total_distance % 3 == 0)

      # Calculate antinodes
      antinode1, antinode2 = p1.find_antinodes(p2)

      # Add antinodes if they're within map bounds
      antinodes.add(antinode1) if map.contains(antinode1)
      antinodes.add(antinode2) if map.contains(antinode2)
    end
  end

  antinodes.size
end

map = Map.new("input.txt")
puts find_antinodes(map)

4. ניסיון 3 - קוד המחלקות עצמן

פה הבנתי ש API Docs זה אולי לא הדרך הכי טובה לעודד את קלוד לעבוד והלכתי לנסות כיוון אחר והדבקתי לקלוד את הקוד המלא של שתי המחלקות Map ו Point.

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

# Solution for the Easter Bunny antenna problem

# Load the map
map = Map.new('input.txt')

# Track unique antinode locations using a hash for O(1) lookup
antinode_locations = {}

# For each frequency
map.index_by_letter.each do |freq, antennas|
  # Skip if there's only one antenna with this frequency
  next if antennas.length < 2

  # Process each pair of antennas with the same frequency
  antennas.combination(2) do |p1, p2|
    # The find_antinodes method calculates potential antinode points
    # based on the relative positions of p1 and p2
    antinode1, antinode2 = p1.find_antinodes(p2)

    # Check both potential antinodes
    [antinode1, antinode2].each do |node|
      # Skip if the node is outside the map boundaries
      next unless map.contains(node)

      # Calculate Manhattan distances from the node to each antenna
      dist1 = (node.x - p1.x).abs + (node.y - p1.y).abs
      dist2 = (node.x - p2.x).abs + (node.y - p2.y).abs

      # An antinode occurs when one antenna is exactly twice as far away as the other
      if dist1 == 2 * dist2 || dist2 == 2 * dist1
        # Store this antinode location (using a string key to ensure uniqueness)
        key = "#{node.x},#{node.y}"
        antinode_locations[key] = node
      end
    end
  end
end

# The answer is the number of unique antinode locations
puts "The number of unique locations containing an antinode is: #{antinode_locations.size}"

הטעות של קלוד בפיתרון השלישי מעניינת - תחילה הוא מפעיל את הפונקציה שלי כדי למצוא את ה Antinodes:

antinode1, antinode2 = p1.find_antinodes(p2)

אבל אז הוא מוודא שהתוצאות של הפונקציה באמת מקיימות את תנאי המרחק השווה:

dist1 = (node.x - p1.x).abs + (node.y - p1.y).abs
dist2 = (node.x - p2.x).abs + (node.y - p2.y).abs

# An antinode occurs when one antenna is exactly twice as far away as the other
if dist1 == 2 * dist2 || dist2 == 2 * dist1
  # Store this antinode location (using a string key to ensure uniqueness)
  key = "#{node.x},#{node.y}"
  antinode_locations[key] = node
end

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

5. מסקנות

אז מה למדנו מהניסוי? בינתיים לא הרבה. הנה כמה מסקנות שאני לוקח:

  1. בעבודה עם AI חשוב לנסח בצורה מדויקת את הבעיה.

  2. קלוד לא תמיד מאמין לתיעוד או למימוש שלנו. בינתיים לא הצלחתי לשכנע אותו לכתוב את הקוד בלי להוסיף את הבדיקה הכפולה.

  3. מצד שני קלוד שמח להשתמש במימוש שלי ל Map ולא ניסה לכתוב בעצמו קוד שמפרסס את הקלט.

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

  5. אני חושב שבמערכת גדולה בעבודה עם Cursor כשאנחנו רוצים לוודא ש AI משתמש באבסטרקציות שאנחנו כותבים (למשל לוודא שהוא משתמש ב Custom Hook שכתבנו במקום לכתוב חדש) כדאי להוסיף לקונטקסט של ה Prompt גם דוגמת קוד שמשתמשת באותה אבסטרקציה. אולי זה אפילו יותר חשוב מהקוד המקורי של האבסטרקציה.

  6. אנחנו עדיין לומדים. בעבודה עם AI מה שחשוב זה לעשות כמה שיותר ניסויים ולא לקחת "כללי אצבע", לא משנה מי כותב אותם. תראו מה עובד לכם ותמשיכו לבדוק כשיוצאים מודלים חדשים.