Spoiler alert

This is a full solution for Advent of Code 2025, day 9 puzzles with explanations.

If you want to try to solve it yourself first, head over to https://adventofcode.com/2025/day/9 and give it a go and then come back and compare notes.

You can find the full code at https://github.com/Hamatti/adventofcode-2025/blob/main/src/day_9.py

We’re ramping up the difficulty with grid-based puzzles today when we arrive at the dance floor (my interpretation) of a movie theater. The Elves have put down red and green tiles and want to find out how large areas these red tiles surround.

Read input

Our input is once again coordinates, this time in 2D space:

7,1
11,1
11,7
9,7
9,5
2,5
2,3
7,3

So we’ll read them into Coordinate namedtuples:

from utils import read_input, Coordinate
 
def mapper(line: str) -> Coordinate:
    x, y = [int(num) for num in line.split(',')]
    return Coordinate(x, y)

Part 1

In the first part, we need to find the largest rectangle possible where the opposite corner tiles are red.

def part_1() -> int:
    coordinates = read_input(9, mapper)
 
    largest = 0
    for c1, c2 in combinations(coordinates, 2):
        area = (abs(c1.x - c2.x) + 1) * (abs(c1.y - c2.y) + 1)
        if area > largest:
            largest = area
    return largest

We create all pairs of red tile locations, calculate the area they cover and find the largest one.

Part 2

In the second part, we get some extra instructions. Turns out, the red tiles create a perimeter and between every consecutive two red tiles in the input, there’s a line of green tiles connecting them and the area inside the formed larger area is filled with green tiles too.

If # is red tile and X is a green tile, it means this:

..............
.......#XXX#..
.......XXXXX..
..#XXXX#XXXX..
..XXXXXXXXXX..
..#XXXXXX#XX..
.........XXX..
.........#X#..
..............

Now the goal is to find the largest rectangle area within that space where opposite corners are once again red tiles.

We can go through all the possible boxes, sort them by their area (so that we start looking from the largest and stopping once we find one) and then check if they are inside the original formation.

def part_2() -> int:
    coordinates = read_input(9, mapper)
    
    areas = [(c1, c2, calculate_area(c1, c2)) for c1, c2 in combinations(coordinates, 2)]
    areas_by_size = sorted(areas, key=lambda x: x[2], reverse=True)
    lines = list(pairwise(coordinates + [coordinates[0]]))
 
    for c1, c2, area in areas_by_size:
        # Boundaries of the rectangle
        left = min(c1.x, c2.x)
        right = max(c1.x, c2.x)
        top = min(c1.y, c2.y)
        bottom = max(c1.y, c2.y)
 
        # Does any of the lines in input cross through our rectangle?
        # If it does, part of the area is outside and we go to the next area
        if any(
            not (
                max(start.x, end.x) <= left
                or right <= min(start.x, end.x)
                or max(start.y, end.y) <= top
                or bottom <= min(start.y, end.y)
            ) for start, end in lines
        ):
            continue
 
        # If none of the lines intersect, we've found an area that's fully inside
        return area

I’m pretty happy with the result functionally but not how it reads. The any part especially is hard to read.

So I went back to the drawing board, trying to give names to things as well as I could to help myself and all of you understand what happens. So let’s give it a second go:

def intersects(c1: Coordinate, c2: Coordinate, line: Tuple[Coordinate, Coordinate]) -> bool:
    """Checks if a line between two coordinates intersects with a rectangle drawn between
    opposite corners c1 and c2"""
    # Boundaries of the rectangle
    rect_left = min(c1.x, c2.x)
    rect_right = max(c1.x, c2.x)
    rect_top = min(c1.y, c2.y)
    rect_bottom = max(c1.y, c2.y)
    
    l1, l2 = line
 
    # Positions of the line
    # If line is vertical, line_right == line_left
    # If line is horizontal, line_top == line_bottom
    line_right = max(l1.x, l2.x)
    line_left = min(l1.x, l2.x)
    line_bottom = max(l1.y, l2.y)
    line_top = min(l1.y, l2.y)
 
    if line_right == line_left:
        vertical_line_is_inside = rect_right > line_right > rect_left
        is_within_box_horizontally = line_bottom > rect_top and rect_bottom > line_top
        return vertical_line_is_inside and is_within_box_horizontally
 
    else:
        horizontal_line_is_inside = rect_bottom > line_top > rect_top
        is_within_box_vertically = rect_right > line_left and line_right > rect_left
        return horizontal_line_is_inside and is_within_box_vertically

I made a very verbose intersects function that checks if a line crosses through our area. First, we find all corners of the rectangle and the extremes of the line.

If the line goes up-down, its x coordinate stays the same and if it goes left-right, its y coordinate stays the same. This solution doesn’t require this if/else structure at all but I wrote it to help figure out what actually goes on by giving names to things. It can be extremely powerful.

In the case of up-down line, we check if that line is within the left-right boundaries of the rectangle. If it does, we’re likely dealing with something that pierces through the area. For example, if we have an area like this (X marks any cell that’s part of the area here):

. . . . . 
. X X X .
. X X X .
. X X X .
. . . . .

and we have a vertical line going through x=2

. . | . . 
. X | X .
. X | X .
. X | X .
. . | . .

However, we still need to check whether it enters our box from top or bottom. That’s our is_within_box_horizontally. Really difficult concepts to explain in variable names.

And if it’s a horizontal line, we do the same but opposite.

Now we have end up with a cleaner part 2:

def part_2() -> int:
    coordinates = read_input(9, mapper)
    
    areas = [(c1, c2, calculate_area(c1, c2)) for c1, c2 in combinations(coordinates, 2)]
    areas_by_size = sorted(areas, key=lambda x: x[2], reverse=True)
    lines = list(pairwise(coordinates + [coordinates[0]]))
 
    for c1, c2, area in areas_by_size:
        # Does any of the lines in input cross through our rectangle?
        # If it does, part of the area is outside and we go to the next area
        if any(intersects(c1, c2, line) for line in lines):
            continue
 
        # If none intersect, we found a winner
        return area

I think it reads better and while the intersects function is very verbose, it helped me understand what those calculations really mean.

Addendum A: a possible missing corner case

Ingo van Lil kindly pointed out that my solution has an invalid corner case .

In a case like

. . . . .
. . . X X
. . . X X
. . . X X
X X X X X
X X X X X

my solution would say that

 . . . .  .
[. . . X] X
[. . . X] X
[. . . X] X
[X X X X] X
 X X X X  X

is a valid rectangle because it doesn’t intersect with any of the lines, only brushes on them.

I don’t have the spoons to figure it out right now so I’ll accept my partly incomplete double star and come back to that when I have more energy to look at it again.

Get in touch

If you want to comment on these solutions, I share them in Mastodon where you can look for the post for the right date or you can start a discussion over email with juhamattisantala@gmail.com.

Follow via RSS

I have created a custom RSS feed for these solutions so you can follow along and not miss any of them!