Day 14

# puzzle prompt: https://adventofcode.com/2024/day/14

import sys
import time

sys.path.insert(0, "..")

import operator
import re
from collections import Counter
from functools import reduce

from base.advent import *


class Solution(InputAsLinesSolution):
    _year = 2024
    _day = 14

    _is_debugging = False

    WIDTH = 101  # magic numbers from the puzzle
    HEIGHT = 103

    def GetSpaceForTest(self):
        self.WIDTH = 11  # magic numbers from the puzzle
        self.HEIGHT = 7

    def GetRobots(self, input):
        robots = []

        for line in input:
            px, py, vx, vy = map(int, re.findall(r"(-?\d+)", line))
            robots.append((px, py, vx, vy))

        return robots

    def EvaluatePosition(self, robot, time):
        px, py, vx, vy = robot

        return ((px + vx * time) % self.WIDTH, (py + vy * time) % self.HEIGHT)

    def FindQuadrant(self, position):
        px, py = position

        if px == self.WIDTH // 2 or py == self.HEIGHT // 2:
            return None

        return (px < self.WIDTH // 2, py < self.HEIGHT // 2)

    def CountRobots(self, input):
        robots = self.GetRobots(input)

        quadrants = Counter()

        for robot in robots:
            new_position = self.EvaluatePosition(robot, 100)
            quadrant = self.FindQuadrant(new_position)
            if quadrant is not None:
                quadrants[quadrant] += 1

        safety_factor = reduce(operator.mul, map(lambda v: v, quadrants.values()), 1)

        return safety_factor

    def FindAdjacent(self, positions):
        adjacents = 0
        pos_set = set(positions)

        for y in range(self.HEIGHT):
            for x in range(self.WIDTH - 1):
                if (x, y) in pos_set and (x + 1, y) in pos_set:
                    adjacents += 1

        return adjacents

    # the tree must be a set of connected positions i.e. adjacent to each other
    def FindEasterEgg(self, input):
        robots = self.GetRobots(input)

        for time in range(
            10000
        ):  # arbitrary time limit, tried with 1000, 5000, then 10000
            positions = [self.EvaluatePosition(robot, time) for robot in robots]
            adjacencies = self.FindAdjacent(positions)
            if adjacencies > 100:  # arbitrary number of points
                # self.draw(positions)
                return time

        raise Exception("Easter egg not found")

    def draw(self, positions):
        grid = [[" " for _ in range(self.WIDTH)] for _ in range(self.HEIGHT)]
        pos_set = set(positions)

        for y in range(self.HEIGHT):
            for x in range(self.WIDTH - 1):
                if (x, y) in pos_set:
                    grid[y][x] = "#"
                else:
                    grid[y][x] = " "

        for row in grid:
            print((" ".join(row)))

    def pt1(self, input):
        self.debug(input)

        safety_factor = self.CountRobots(input)

        return safety_factor

    def pt2(self, input):
        self.debug(input)

        time = self.FindEasterEgg(input)

        return time

    def part_1(self):
        start_time = time.time()

        res = self.pt1(self.input)

        end_time = time.time()

        self.solve("1", res, (end_time - start_time))

    def part_2(self):
        start_time = time.time()

        res = self.pt2(self.input)

        end_time = time.time()

        self.solve("2", res, (end_time - start_time))


if __name__ == "__main__":
    solution = Solution()

    solution.part_1()

    solution.part_2()