Coding

Coding with Perplexity AI – Hirst Painting Drawer

Ive not been using AI a lot, frankly, I find it to be pretty lame for the most part, the images are almost always weirdly uncanny and ugly, and the writing is just bland. I’ve heard it’s pretty good at coding though, and I have not tried using it for code at all. So I decided to give it a go. Specifically, I wanted to use it to augment an existing project from when I was taking that 100 Days of Python course. Specifically, Day 18, the Hirst Painting Project.

The full original code is here:

import colorgram
from turtle import Turtle, Screen
from random import choice
turtle.colormode(255)

#Sample image, cover of CHVRCHES Every Open Eye Album
color_extracted = colorgram.extract("image.jpg", 20)
color_choices = []
for i in color_extracted:
    color_choices.append((i.rgb.r, i.rgb.g, i.rgb.b))

# debug print(color_choices)
#color_choices = [(193, 137, 150), (128, 74, 88), (22, 28, 47), (59, 32, 48), (219, 210, 206), (184, 161, 155), (17, 11, 11), (174, 101, 116), (217, 179, 189), (148, 152, 159), (94, 47, 60), (93, 104, 114), (227, 201, 206), (154, 159, 156), (122, 83, 78), (209, 183, 180), (203, 206, 202), (164, 109, 105), (81, 95, 91), (59, 60, 74)]


marker = Turtle()
marker.speed(100)
marker.penup()
marker.hideturtle()

for y_coord in range(1,10):
    for x_coord in range(0,10):
        marker.setpos(-300 + x_coord * 50,-280 + y_coord * 50)
        marker.pencolor(choice(color_choices))
        marker.dot(30)

screen = Screen()
screen.screensize(600, 600)
screen.exitonclick()

This code, which is also on GitHub with the sample, image, but any image will work, will read the color pallet of a file, “image.jpg”, then draws a simple series of dots in the style of a painting by Damien Hirst. I didn’t pick the theme, it was part of the course, but I do think the result is simple and quite neat.

I have for a while, wanted to make a few updates to this simple program, and sort of tried to a few times, but this time, I let AI do the work. I really wanted two main features.

  • The ability to open any file, instead of having to put a file in the folder and rename it image.jpg
  • The ability to export the result to an image file

I chose Perplexity AI for my assistant. I wanted to use it as a sort of, accompanying tool, rather than letting ti write all the code. I already have the simple drawing code.

I started by asking it for a simple request:

Can you create a python script that will open a turtle graphics window, 1024x768 in size, draw a circle of 5 pixels thickness, diameter 100 pixels, and include a button that will export the canvas to a png or jpg file

Which it did, I could run the code, it would draw a circle, then I could click a button and save an image of a circle. Though, I did come across an issue I never quite fixed.

It would save the dialogue box along with the canvas. It’s basically just, taking a screen shot.

The solution at the moment is to make sure I drag the dialog box off to the side before saving.

Next:

Can you add a "file Open" button at the bottom that passes the file in as a variable and does not draw anything until a file as been selected

Initially, I wanted to make sure it wouldn’t draw an image until the file was loaded, so the file select box doesn’t actually do anything. Later I changed it to allow for drawing without selecting an image, it just defaults to the ‘Every Open Eye’ color set in my original code.

This worked out as expected as well. Now I had the basic structure to slip my existing code in. I had a file as a variable and a mechanism that drew something (currently, a circle). The code it was giving me though, used a class structure though, which is fine, but my existing code doesn’t. I managed to insert my dots drawing code fine, this required renaming some variables to align, specifically, all of the ‘marker’ variables became “self.pen” at the appropriate location. I had trouble though getting the colormode to work properly. I wasn’t sure where to put it in the code, as relating to the class structure.

I have to say, I probably had it correct, but I also realized later I was having some virtual environment issues between VS Code and my venv and the system. Despite VSCode showing that the imports were resolved, when running things, I got not found’ errors. I ended up just running the code from a venv sourced terminal outside of VSCode. It”s a problem to be fixed later.

The first problem that came up here though, Perplexity had added a function that would display the loaded image, as a backdrop behind the dots. This is not the functionality I wanted.

I just found the function and stripped it out manually.

Then I found it again, because it loaded the image as a backdrop when opening a file, and then again when drawing the dots.

Something also notable here. At one point, I took my working code, with the draw dots, and fed it back to Perplexity, telling it, ‘I added some code, please make this the new baseline.’ This worked out, perfectly. Going forward, it worked off my updated code. Even more surprisingly, it detected the new function of the drawing, to draw dots, instead of a single circle, and it renamed the internal references ON ITS OWN.

I was pretty impressed with that.

I had the basic functionality down, but wanted to do some cleanup. After running it over and over, and having to navigate to a directory with pictures each time, I asked it to change the file open and save to default to the user’s home folder. I also asked it to only look for image files, to avoid errors from other file types. I also had it resize the image down to center and fit the dots better.

I couldn’t solve the dialogue box option. I tried to, It added a short delay on the save, but that just reulted in a saved image of the things behind the drawing. I tried to get it to position the save dialog outside of the window, but the code there didn’t seem to actually DO anything.

I also added a few last-minute features. One, is a way to update the background color. It had actually had this feature originally, but I asked it to remove it, because at the time, I didn’t want it.

I also had it add some boxes that allow for selecting how many rows and columns will be drawn. I may look into having it draw larger canvases or maybe things that are not dots in the future. It’s pretty functional as it is though. Well, at least as functional as a program that makes dot images can be.

Anyway, I’ll make a GitHub Repository probably, but for now, the full updated code is below. Also, it turns out I can share my Perplexity chat, so you can also see the full chat here.

## pip install colorgram.py

import turtle
import colorgram
from tkinter import *
from tkinter import filedialog
from tkinter import colorchooser
from random import choice
from PIL import ImageGrab
import os

class TurtleApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Turtle Graphics with Image Open and Save")
        
        self.canvas_width = 600
        self.canvas_height = 550
        
        self.canvas = Canvas(root, width=self.canvas_width, height=self.canvas_height)
        self.canvas.pack()
        
        self.screen = turtle.TurtleScreen(self.canvas)
        self.screen.colormode(255)
        self.screen.bgcolor("white")
        
        self.pen = turtle.RawTurtle(self.screen)
        self.pen.pensize(5)
        self.pen.hideturtle()
        
        self.file_path = None
        self.color_choices = [(193, 137, 150), (128, 74, 88), (22, 28, 47), (59, 32, 48), (219, 210, 206), 
                              (184, 161, 155), (17, 11, 11), (174, 101, 116), (217, 179, 189), (148, 152, 159), 
                              (94, 47, 60), (93, 104, 114), (227, 201, 206), (154, 159, 156), (122, 83, 78), 
                              (209, 183, 180), (203, 206, 202), (164, 109, 105), (81, 95, 91), (59, 60, 74)]
        
        self.rows = 9
        self.columns = 10
        
        self.create_widgets()

    def create_widgets(self):
        button_frame = Frame(self.root)
        button_frame.pack(side=BOTTOM, fill=X)

        open_button = Button(button_frame, text="Open Image", command=self.open_file)
        open_button.pack(side=LEFT, padx=5, pady=5)
        
        draw_button = Button(button_frame, text="Draw Dots", command=self.draw_dots)
        draw_button.pack(side=LEFT, padx=5, pady=5)
        
        save_button = Button(button_frame, text="Export Canvas", command=self.save_canvas)
        save_button.pack(side=LEFT, padx=5, pady=5)

        bg_color_button = Button(button_frame, text="Change Background", command=self.change_background_color)
        bg_color_button.pack(side=LEFT, padx=5, pady=5)

        exit_button = Button(button_frame, text="Exit", command=self.exit_app)
        exit_button.pack(side=LEFT, padx=5, pady=5)

        # Add row and column input
        row_label = Label(button_frame, text="Rows:")
        row_label.pack(side=LEFT, padx=5, pady=5)
        self.row_entry = Entry(button_frame, width=5)
        self.row_entry.insert(0, str(self.rows))
        self.row_entry.pack(side=LEFT, padx=5, pady=5)

        col_label = Label(button_frame, text="Columns:")
        col_label.pack(side=LEFT, padx=5, pady=5)
        self.col_entry = Entry(button_frame, width=5)
        self.col_entry.insert(0, str(self.columns))
        self.col_entry.pack(side=LEFT, padx=5, pady=5)

    def open_file(self):
        # Get the user's home directory
        home_dir = os.path.expanduser("~")
        
        self.file_path = filedialog.askopenfilename(
            initialdir=home_dir,  # Set initial directory to user's home folder
            filetypes=[
                ("Image files", "*.png *.jpg *.jpeg *.gif *.bmp"),
                ("PNG files", "*.png"),
                ("JPEG files", "*.jpg *.jpeg"),
                ("GIF files", "*.gif"),
                ("BMP files", "*.bmp")
            ]
        )
        if self.file_path:
            print(f"Image selected: {self.file_path}")
            self.root.title(f"Turtle Graphics - {os.path.basename(self.file_path)}")
            self.extract_colors()

    def change_background_color(self):
        color = colorchooser.askcolor(title="Choose background color")
        if color[1]:  # color is in the format ((r, g, b), hexcode)
            self.screen.bgcolor(color[1])
            print(f"Background color changed to {color[1]}")

    def extract_colors(self):
        color_extracted = colorgram.extract(self.file_path, 20)
        self.color_choices = []
        for i in color_extracted:
            self.color_choices.append((i.rgb.r, i.rgb.g, i.rgb.b))
        print("Colors extracted from the image")

    def draw_dots(self):
        self.pen.clear()
        
        try:
            self.rows = int(self.row_entry.get())
            self.columns = int(self.col_entry.get())
        except ValueError:
            print("Invalid row or column value. Using default values.")

        self.pen.speed(100)
        self.pen.penup()
        self.pen.hideturtle()

        dot_size = 30
        spacing_x = self.canvas_width / (self.columns + 1)
        spacing_y = self.canvas_height / (self.rows + 1)
        start_x = -self.canvas_width / 2 + spacing_x
        start_y = self.canvas_height / 2 - spacing_y

        for y_coord in range(self.rows):
            for x_coord in range(self.columns):
                self.pen.setpos(start_x + x_coord * spacing_x, start_y - y_coord * spacing_y)
                self.pen.pencolor(choice(self.color_choices))
                self.pen.dot(dot_size)

    def save_canvas(self):
        # Get the main window's position and size
        window_x = self.root.winfo_x()
        window_y = self.root.winfo_y()
        window_width = self.root.winfo_width()
        
        # Calculate the position for the dialog box
        dialog_x = window_x + window_width + 10  # 10 pixels to the right of the main window
        dialog_y = window_y
        
        # Get the user's home directory
        home_dir = os.path.expanduser("~")
        
        # Open the save dialog at the calculated position
        self.root.update()  # Ensure the window size is updated
        save_path = filedialog.asksaveasfilename(
            parent=self.root,
            defaultextension=".png",
            filetypes=[("PNG files", "*.png"), ("JPEG files", "*.jpg *.jpeg")],
            initialdir=home_dir,  # Set initial directory to user's home folder
        )
        
        if save_path:
            # Move the dialog to the desired position
            self.root.geometry(f"+{dialog_x}+{dialog_y}")
            
            x0 = self.root.winfo_rootx() + self.canvas.winfo_x()
            y0 = self.root.winfo_rooty() + self.canvas.winfo_y()
            x1 = x0 + self.canvas.winfo_width()
            y1 = y0 + self.canvas.winfo_height()
            
            ImageGrab.grab(bbox=(x0, y0, x1, y1)).save(save_path)
            print(f"Canvas saved as {save_path}")
            
            # Reset the main window position
            self.root.geometry(f"+{window_x}+{window_y}")

    def exit_app(self):
        self.root.quit()
        self.root.destroy()

root = Tk()
app = TurtleApp(root)
root.mainloop()

Advent of Code 2024 – Day 06 – Guard Gallivant

It’s happening already. The slow crushing creep of laziness. I have not done part 2, yet, and I will, maybe get to it. I have ideas for how to solve part 2, its just… Kind of tedious and I want a respite.

Part 1 was fun though. Basically just, a simple pathfinding robot to trace the path of a guard through a maze of obstacles. I even gave it the ability to draw the map, but the dataset is bigger than my monitor so the refresh and drawing it’s stupid ugly with the real data set. I also turned it off because it drags the entire process to a crawl. A seconds-long calculation takes minutes and counting when its drawing nonsense.

The sample dataset looks neat though when drawn.

Part 2 is to find every place you could drop an object to create a loop for the guard. I thought I had it, I had the right idea, but I only ended up finding all of the existing loops. And I am not sure some were not in the middle of “walls”.

I got to thinking about what I did wrong, which is basically, checking for squares (loops) instead of finding them. I got to thinking about new algorithms to check all directions instead of just one, but what I need to do is to check along each side for walls and see if the gap is longer than the previous sides.  But that’s not going to work exactly, so I think I can use the distances traveled to see if there are places to drop objects based on if sides are shorter than the previous sides.

That probably made more sense in my head.

It probably isn’t that hard, I just, don’t feel like doing it right now. Occasionally there is a stupid simple one thrown in that takes 5 minutes to do both parts, maybe I can come back to today on that day.

Or maybe just sometime tomorrow, tomorrow is surprisingly pretty open.

Anyway, here is the code, I trimmed out the useless part 2 function.

import time
import os

with open("Day06Input.txt") as file:
    data = file.read()

lines = data.split("\n")
lines.pop()

guard = [0,0,0]
grid = []
guard_active = True
total = 0
total2 = 0
distances = []
cur_steps = 0

for each in lines:
  if "^" in each:
    guard[0] = each.index("^")
    guard[1] = lines.index(each)
#  print(guard)
  grid.append(list(each))

def print_grid(thegrid):
  for each in thegrid:
    print("".join(each))

#print_grid(grid)

def move_guard(thegrid, guard_pos):
# 0 - North, 1 - East, 2 - South, 3 - West
# Defind in guard[2]
# guard[0] = x coord (across rows), guard [1] = y coord (up and down lines)
  step = 1
  if (thegrid[guard_pos[1]-1][guard_pos[0]] != "#") and guard_pos[2] == 0:
    thegrid[guard_pos[1]][guard_pos[0]] = "X"
    thegrid[guard_pos[1]-1][guard_pos[0]] = "^"
    guard_pos[1] = guard_pos[1]-1
  elif (thegrid[guard_pos[1]][guard_pos[0]+1] != "#") and guard_pos[2] == 1:
    thegrid[guard_pos[1]][guard_pos[0]] = "X"
    thegrid[guard_pos[1]][guard_pos[0]+1] = "^"
    guard_pos[0] = guard_pos[0]+1
  elif (thegrid[guard_pos[1]+1][guard_pos[0]] != "#") and guard_pos[2] == 2:
    thegrid[guard_pos[1]][guard_pos[0]] = "X"
    thegrid[guard_pos[1]+1][guard_pos[0]] = "^"
    guard_pos[1] = guard_pos[1]+1
  elif (thegrid[guard_pos[1]][guard_pos[0]-1] != "#") and guard_pos[2] == 3:
    thegrid[guard_pos[1]][guard_pos[0]] = "X"
    thegrid[guard_pos[1]][guard_pos[0]-1] = "^"
    guard_pos[0] = guard_pos[0]-1
  else:
    guard_pos[2] = (guard_pos[2]+1) % 4
    step = 0
#  print(guard_pos)

  return thegrid, guard_pos, step


while guard_active:
  this_step = 0
  grid, guard, this_step = move_guard(grid, guard)
  if this_step == 0:
    distances.append(cur_steps)
    cur_steps = 0
  else:
    cur_steps += this_step
# Optionally Print the Map
#  print_grid(grid)
#  time.sleep(.001)
#  os.system('clear')
#  print(guard)
#  print(len(grid)-1)
  if guard[1] >= len(grid)-1 or guard[1] <= 0 or guard[0] >= len(grid[0])-1 or guard[0] <= 0:
    grid[guard[1]][guard[0]] = "X"
    guard_active = False
    print("The guard has left the area!")

for each in grid:
  total+= each.count("X")

#Print the final grid for fun
#print_grid(grid)

print(distances)

print(total)
# 5318
print(total2)

Advent of Code 2024 – Day 05 – Print Queue

Well, it’s an easy and hard-ish day.  I kind of hate the part 2s on these because they are almost always annoying.  It was a bit more interesting on the input because it was a two part input.  I’m still getting weird empty spaces at the end too, which I have just been trimming with a .pop(), but that’s a bit sloppy.  Today’s puzzle is verifying sorting of sets of numbers.

Part 1 was pretty simple, mostly because I forced myself not to “overthink it”.  I just, compared every pair set to the valid list.  Then if they were valid, added them into the total.  It feels like it’s going to miss some things, but it didn’t.  I got the correct answer in one try.

Part 2 was a pain.  It was, “take the wrong answers and fix them”.  What I did initially was just, swap the numbers if they were wrong.  This would put them one step closer to correct, afterwards, it fed them back through the check, this time flagged as bad, since the two sets need to be answered separately.  This code worked for the sample input for both parts. 

When I ran it through my actual data, this resulted in an answer of “5692”, which was incorrect.  Everything seemed correct in the code.  I even created a copy of the sorting loop with a “total3”, this time just running through bad pages.  It matched the answer of 5692.  So I started trying to guess a bit to see if I was even close I was within 100 for sure.  I went off to Reddit and pulled someone else’s code, ran my data set through it, and got the answer “5770”, which is the correct answer.

It also gave me a direction on where to look.  I tried a different sort where I just, slapped the first number at the end and sorted it back through.  It returned the same 5692.  Which would be a good sign, that both sorts were getting the same results, if the answer was correct.  On a bit of a lark, I decided to see just how off I was, 5770-5692 is 78.  In the data, on the last line, is a 78.  This was the key to solving the problem.  I uncommented some of my print statements and, sure enough, it wasn’t running the last line.

You know that .pop() I mentioned back at the start of this?  Well, it turns out that the sample data had an extra blank.  My actual data, did not, so it was just, dropping the last line of data.

Remember when I said it was a bad idea?

with open("Day05Input.txt") as file:
    data = file.read()


rules = data.split("\n\n")[0].split("\n")
pages = data.split("\n\n")[1].split("\n")
#pages.pop()
bad_pages = []

#Verify Inputs
#for each in pages:
#for each in rules:
#  print(each)

total = 0
total2 = 0
total3 = 0

for each in pages:
  these_pages = each.split(",")
  goodset = True
  running = True
#  print(these_pages)
  while running:
#    print(these_pages)
    running = False
    restart = False 
    for p1 in these_pages:
      for p2 in these_pages[these_pages.index(p1):]:
        if p1 != p2:
          pair = f"{p1}|{p2}"
          if pair in rules:
            #print("Good")
            pass
          else:
#            print(these_pages.index(p1))
            p1pos = these_pages.index(p1)
            p2pos = these_pages.index(p2)
            these_pages[p1pos] = p2
            these_pages[p2pos] = p1
#            print(these_pages.index(p1))
            #print("Bad")
            #print(pair)
            running = True
            goodset = False
 #           print("---")
 #           print(these_pages)
  if goodset:
    #print(f"Set {pages.index(each)} is good")
    total += int(these_pages[int((len(these_pages) - 1)/2)])
  else:
    bad_pages.append(these_pages)
    total2 += int(these_pages[int((len(these_pages) - 1)/2)])



for these_pages in bad_pages:
  goodset = True
  running = True
#  print(these_pages)
  while running:
#    print(these_pages)
    running = False
    restart = False 
    for p1 in these_pages:
      for p2 in these_pages[these_pages.index(p1):]:
        if p1 != p2:
          pair = f"{p1}|{p2}"
          if pair in rules:
            #print("Good")
            pass
          else:
            these_pages.append(these_pages.pop(p1))
#            print(these_pages.index(p1))
#            print(these_pages.index(p1))
            #print("Bad")
            #print(pair)
            running = True
            goodset = False
 #           print("---")
 #           print(these_pages)
  total3 += int(these_pages[int((len(these_pages) - 1)/2)])




print(total)
print(total2)
# Part 1 - 7365
#5692 Low
#5700 Low
#5800 Too High
#5750 - Wrong
#5730 - Wrong
print(total3)
## Its not working but the answer for future reference is 5770

Advent of Code 2024 – Part 04 – Ceres Search

Weee, the first real “pain in the ass” day and it’s only day 4!

Ok, it wasn’t awful, I had the right idea, for the most part.  I wanted to do something better for Part 2, but it wasn’t working out, but I’ll get there.  For this puzzle, you get a blog of text with the letters of XMAS in them, randomly, scattered.  Sometimes it actually spells XMAS, it’s a “crossword” puzzle.  The ides is to find all of the occurrences of XMAS, crossword style, front, back, up down, and diagonally.  I used Numpy here to do some rotations on the letter matrix, which was fun.

First was a simple search for “XMAS” and “SAMX” on every line of the matrix.  Then I looked into how to rotate the matrix, 45 degrees.  Thankfully, there was a way.  I did it twice, one clockwise and one counter clockwise, which gave me all of the “diagonals” as single lines of texts.  I appended this to the normal lines and did the search again.  Then I realized, I needed to also search the verticals.  This meant, another, different, Numpy rotation.  I appended this to the big blob of lines as well, and ran the search again.

This gave the correct answer, easy.

I started out with a good but failed approach on Part 2.  Some of the code is still there, commented out, for posterity.

For Part two, the search is modified, you need to find all occurrences of “X-MAS”.  That is, places where the letters “MAS” make an “X” shape.  They can go forward or backwards, and must occur twice.  I started out by making a second copy of my diagonals lines.  I wanted to construct these back into “square” shapes.  I looked into doing some sort of fancy fill methods, but instead just, did a simple algorithm based on line lengths.  The idea was, to search for the As in one rotation block, then check if it’s a MAS or a SAM, then check the matching A on the opposite rotation block of text.  These coordinates should be opposites.  If point 2,4 is an “A”, then on the opposite rotation, that same A should be at point (1,2).

Except…  it’s not.  There is some funny doubling going on with one of the coordinates. I spent, more time than I care to admit trying to puzzle it out.  I finally gave up.

Instead I did the old fashioned “brute force” method.  I took the original block of text, checked each character to see if it’s an A, if it was an A, I built the two cross words, and checked if they were SAM or MAS.  This all occurs before modifying the block of text for part 1.

I got stuck a bit on it again, because I made a stupid typo and used “i” instead of “j” when making my check words.

Anyway, here is my code below for Day 4 in Python. 

with open("Day04Input.txt") as file:
    data = file.read()

import numpy as np

lines = data.split("\n")
lines.pop()
diaglines = []
total = 0
total2 = 0
grid = []
gridsize = len(lines[1])

for each in lines:
  grid.append(list(each))

a = np.array(grid)

rotated_a = np.rot90(a)

# Part 2 visa the shitty Brute Force Way
for i in range(len(lines)-1):
  for j in range(len(lines[i])-1):
    if i != 0 and j != 0:
#      print(lines[i][j])
      if lines[i][j] == 'A':
        check1 = lines[i+1][j+1]+lines[i][j]+lines[i-1][j-1]
        check2 = lines[i+1][j-1]+lines[i][j]+lines[i-1][j+1]
        if (check1 == "MAS" or check1 == "SAM") and (check2 == "MAS" or check2 == "SAM"):
          total2 += 1
#          print(check1)
#          print(check2)
#          print("Match")
        else:
          print(check1)
          print(check2)
          print(lines[i-1])
          print(lines[i])
          print(lines[i+1])
          print("------")


# Rotate the array 90 and add the lines to our overall lines
for row in rotated_a:
#  print(''.join(row))
  lines.append(''.join(row))

# Rotate the array diagonally
# https://stackoverflow.com/questions/6313308/get-all-the-diagonals-in-a-matrix-list-of-lists-in-python
diags = [a[::-1,:].diagonal(i) for i in range(-a.shape[0]+1,a.shape[1])]
diags.extend(a.diagonal(i) for i in range(a.shape[1]-1,-a.shape[0],-1))
#######################################################################################################


## Join the rotated numpy array and add it to our total lines to ge the diagonals
for n in diags:
  new_string = f"{''.join(str(i) for i in n.tolist())}" 
#  print(new_string)
#  padding = int((gridsize - len(new_string))/2)
#  diag_string = ('o'*padding) + new_string + ('o'*padding)
#  if len(diag_string) % 2 == 0:
#    diag_string = diag_string[:-1]
  lines.append(new_string)
#  diaglines.append(diag_string)

#print(diaglines)

# Count up for Part 1
for each in lines:
#  print(each)
  total+= each.count("XMAS")
  total+= each.count("SAMX")

#dlist1 = diaglines[:len(diaglines)//2]
#dlist2 = diaglines[len(diaglines)//2:]


# Print Totals
print(total)
print(total2)

#1771 = Too low
#2003 = Too Low
#Part 1 - 2427

#3085 Too High
#1312 Too Low
#1900 - Part 2

Advent of Code 2024 – Day 02 – Red-Nosed Reports

This entire Post is one big code block, because I did a stupid thing that felt like a good idea at the time.

#Lets comment the heck out of this one and flow with the process.  So it may look a little ugly.

# I am essentially just... blogging in real time, in the comments now....

#Until the end, I am using the sample data set
# 7 6 4 2 1
# 1 2 7 8 9
# 9 7 6 2 1
# 1 3 2 4 5
# 8 6 4 4 1
# 1 3 6 7 9

# I started by copying Day01, then modifying the top lines to read my Day02Input.txt file, because, lazy.
# Then just deleting everything else below the set processing, because none of it is real useful.
with open("Day02Input.txt") as file:
    data = file.read()

sets = [each.split(" ") for each in data.split("\n")]

# Then I want to test it so I add a print of the result, just to make sure it looks right against the sample data set. 
##### NOTE Removed code will just be commented out, to follow the comments.  I'll mark these as ##
# Tehre will be a lot of prints removed.  These are used gratuitously to verify things are working.

##print(sets)

# This gives
# [['7', '6', '4', '2', '1'], ['1', '2', '7', '8', '9'], ['9', '7', '6', '2', '1'], ['1', '3', '2', '4', '5'], ['8', '6', '4', '4', '1'], ['1', '3', '6', '7', '9'], ['']]
# For some reason I keep getting black sets on these, so I'm just gonna do like day one and clip it.
sets.pop()

##print(sets)

# Part of the test on this one is if the digits are ascending or descending, I have an idea to test this out quickly
# Lets see if it works.

##for each in sets:
##   print(each)
##   if each == each.sort():
##      print("Ascending")
##   print(each.sort())
# Ok, I ran into a prolem here, because it's reading everything in as strings and not integers.

# There is probably a more elegant way to do this but this will work.
int_sets = []

for each in sets:
   int_sets.append(list(map(int, each)))

## print(int_sets)

# [[7, 6, 4, 2, 1], [1, 2, 7, 8, 9], [9, 7, 6, 2, 1], [1, 3, 2, 4, 5], [8, 6, 4, 4, 1], [1, 3, 6, 7, 9]]
# Perfect, let's try again.
# Also it looks like I needed to use a different sort, for reasons.
# https://stackoverflow.com/questions/403421/how-do-i-sort-a-list-of-objects-based-on-an-attribute-of-the-objects

##for each in int_sets:
##   if each == sorted(each):
##      print("Ascending")
##   else:
##      print("Bad")

# Bad
# Ascending
# Bad
# Bad
# Bad
# Ascending

# Ok, but I also need Descending Lists

##for each in int_sets:
##   if each == sorted(each):
##      print("Ascending")
##   elif each == sorted(each, reverse=True):
##      print("Descending")
##   else:
##      print("Bad")

# Descending
# Ascending
# Descending
# Bad
# Descending
# Ascending

# Ok, things are working well, I probably need a counter for the number of good lists though.
# Despite these tess and comments, I am still "at the top" of the pure code so.

good_lists = 0

# I also need to check for the other condition in the test
# Any two adjacent levels differ by at least one and at most three.

# Since I am still "at the top", I will go ahead and just make a function to check this here.
# Note to self, post a clean version of the code below.

def check_range(a,b):
##  print(f"{a},{b}")
##  print(abs(b-a))
##  if abs(b-a) >= 3 and abs(b-a) <= 1:
  if abs(b-a) <= 3 and abs(b-a) >= 1:
##    print("OK")
    return True
  return False

# I am also going to clean up my ascending/Descending loop a bit.

##for each in int_sets:
##   if each == sorted(each) or each == sorted(each, reverse=True):
##      for i in range(len(each)-1):
##         print(i)
##         print(f"{each[i]},{each[i+1]}")
##         check_range(each[i], each[i+1])
##      print("Good")
##   else:
##      print("Bad")

# Something is not working right, so I'm modifying the defined function above a bit and adding some prints to test.

# I found it, my Greater than less than signs were reversed, oops.
# Also, all this commenting is seriously slowing this whole process

# I'm copying the above loop though, for posterity, before finding a good way to track good loops.

for each in int_sets:
   if each == sorted(each) or each == sorted(each, reverse=True):
# Lets assume a set is true
      good_set = True
      for i in range(len(each)-1):
# If the set is still good, keep checking, otherwise, don't.
         if good_set:
           good_set = check_range(each[i], each[i+1])
# I hate having these conditionals like this, but this is starting to get exhausting typing this, and it's easy.
# If the set is still good after checking the digits, add one to the total number of good sets.
      if good_set:
         good_lists +=1
# I removed the else, because I don't care if it's bad.

print(good_lists)
# This gives 2, as expected, for the sample dataset.
# Time to adjust for my personal data set as input.

# This gives 326 (data sets and answers very by person).
# This is the correct answer, on to Part 2.

# For Part 2, the system can tolorate a single bad level.  Those are what I am checking for in "check_range'.
# Normally I would modify my code to do both tests at once, but for TODAY, i am just going to make a second version.
# Basically, if a bad pair happens, it needs to try matching with the next higher number.  But it only needs to try it once.

good_lists_p2 = 0

##for each in int_sets:
##   if each == sorted(each) or each == sorted(each, reverse=True):
##      print(each)
##      good_set = True
##      dampened = False
##      for i in range(len(each)-1):
##         if good_set:
##           good_set = check_range(each[i], each[i+1])
##           if not good_set and not dampened:
##              print(f"Trying {each[i]} and {each[i+2]}")
##              print(good_set)
##              good_set = check_range(each[i], each[i+2])
##              print(good_set)
##              dampened = True
##      print(good_set)
##      if good_set:
##         good_lists_p2 +=1
##      print(good_lists_p2)

# So, this is not working out.  It's mostly working, but it's failing my Ascending/Descending check.
# Specifically, this one in the sample data fails. "1 3 2 4 5"

# What I need, is a new way to check Ascending and Descending lists.  An annoying way, that is ugly.
# I could build this in to the existing checks, but at this point, I don't want to, I just need a yes/no check
# And all these notes and comments are getting annoying.

def check_order(list):
   if list[-1] > list[0]:
     direction = "Asc"
   elif list[-1] < list[0]:
     direction = "Desc"
   else:
     direction = "Equal"
#   print(direction)

   for n in range(len(list)-1):
      if list[n+1] > list[n] and direction == "Desc":
        if n+2 < len(list):
          if list[n+2] > list[n]:
             return False
      if list[n+1] < list[n] and direction == "Asc":
        if n+2 < len(list):
          if list[n+2] < list[n]:
             return False
   return True

for each in int_sets:
#   print(each)
   if check_order(each):
      good_set = True
      dampened = False
      for i in range(len(each)-1):
         if good_set:
           good_set = check_range(each[i], each[i+1])
           if not good_set and not dampened and i+2 < len(each):
#             print(f"Trying {each[i]} and {each[i+2]}")
#             print(good_set)
              good_set = check_range(each[i], each[i+2])
#              print(good_set)
              dampened = True
#      print(good_set)
      if good_set:
         good_lists_p2 +=1
#      print(good_lists_p2)






print(good_lists_p2)


# 415 too high
# 414 too high
# 400 too high
# Ok, I started Guessing
# I got it
# 381 for my data set.