Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Work with grids 🔥

Dr. Green grows fresh vegetables for the entire North Pole in a cozy greenhouse tucked behind the workshop. Unfortunately, a few warming pads have quietly failed, and the lettuce has started filing formal complaints. It’s time to find the cold spots before dinner becomes very crunchy and the iceberg lettuce lives up to its name.

Fortunately, every square in the garden has a temperature sensor.

Your puzzle gives you the temperature readings as a grid. Your job is to find the cool spots by comparing each sensor with its neighbors.

Along the way you’ll learn how to represent a grid, convert between coordinates and array indices, walk neighboring cells, and visualize your results.

Mojo inspecting a grid layout. Mojo inspecting a grid layout.

Today’s data

Until now, you’ve mostly worked with strings and lists. Many Advent puzzles instead describe a two-dimensional world: a map, maze, image, game board, or sensor grid.

Although the data is conceptually two-dimensional, it’s usually easiest to store it as a single list. You recover rows and columns with a little arithmetic.

The data

Download grid_temps.txt. Each line packs seven readings together: every two digits is one sensor’s temperature, always below zero, with a magnitude between 00 and 40:

30252832201503
22180520151011
27203022171819
19033231161011
24302230190908

The first row’s pairs, 30 25 28 32 20 15 03, become the readings -30, -25, -28, -32, -20, -15, -3.

Create the project file

Create work_with_grids.mojo, and unpack the digit pairs into a flat, row-major list of temperatures:

    var log = Path("src/downloads/grid_temps.txt")
    var lines = log.read_text().splitlines()

    var rows = len(lines)
    var cols = lines[0].byte_length() / 2
    var data = List[Int]()

    for line in lines:
        for i in range(0, line.byte_length(), 2):
            var pair = line[byte = i : i + 2]
            data.append(-Int(String(pair)))

The list contains five rows of seven readings each.

Convert between indices and coordinates

The readings live in a single list, but puzzles think in rows and columns. You’ll constantly move between the two representations.

Add these helper functions in main() as nested items:

    def index_to_coord(index: Int) {imm} -> Tuple[Int, Int]:
        return (index / cols, index % cols)

    def get_coord_data(row: Int, col: Int) {imm} -> Int:
        return data[row * cols + col]

The first converts a list index into (row, col). The second converts a row and column back into a list index to retrieve the value.

Takeaways

  • {imm} gives the nested functions read-only access to values defined in main(). Here it captures cols and data as immutable references.
  • The data stays in one linear list using row-major ordering.
  • Division finds the row. Modulo finds the column.
  • Returning coordinates as a tuple lets you unpack them naturally.

Before solving the puzzle, it’s helpful to see the data laid out as a grid. Add a place to remember which cells turn out to be cool spots:

    var cool_indices: List[Tuple[Int, Int]] = []

Add a write_data() helper that loops over each row and column, formats the values into aligned columns with ascii_rjust(), and marks any coordinate already in cool_indices with *:

    # Write the data in a grid format
    def write_data() {imm data, imm cool_indices, imm}:
        var print_width = 4
        for row in range(rows):
            for idx in range(row * cols, (row + 1) * cols):
                var item = data[idx]
                var col = idx % cols
                print(t"{String(item).ascii_rjust(print_width)}", end="")
                if (row, col) in cool_indices:
                    print(t"*", end="")
                else:
                    print(t" ", end="")
            if row < rows - 1:
                print("")
        print()

Call it once to check the raw grid, before you’ve searched for anything:

    write_data()

Since cool_indices starts out empty, nothing gets marked yet.

Takeaways

  • ascii_rjust() right-aligns values into fixed-width columns.
  • Nested loops naturally walk rows and columns.
  • The same write_data() you just wrote will show your results later. Calling it again after the search populates cool_indices is the only thing that changes.

Compare neighboring cells

A single reading doesn’t tell you much. Is -30 cold? Compared to what? Is that a cool spot? How do you know? Do you pick a cut-off number?

The interesting part is how each reading compares with the temperatures around it. For this puzzle, a point is considered a cool spot if most of its neighbors are warmer. Remember that with negative readings, warmer means closer to zero, so a plain > comparison still works.

You’ll need to define some puzzle arguments to limit your search and produce your results.

    var radius = 1  # how far to look
    var count = (radius * 2 + 1) ** 2 - 1  # how many neighbors?
    var half_count = count / 2  # half of the neighbors

A radius of one creates a 3×3 neighborhood. Excluding the center leaves eight neighboring cells.

Here’s the heart of your project. For each location, you examine the square neighborhood centered on that spot. Every warmer neighbor increments cooler. If more than half the neighbors are warmer, you’ve found a cool spot.

    for index in range(len(data)):
        var row, col = index_to_coord(index)  # fetch the row and column
        var cooler = 0  # keeps a running comparison count
        var spot = get_coord_data(row=row, col=col)  # the current spot's value

        # Check boundaries for safe indexing
        if not (
            radius <= row < rows - radius and radius <= col < cols - radius
        ):
            continue

        for dRow in range(row - radius, row + radius + 1):
            for dCol in range(col - radius, col + radius + 1):
                if dRow == row and dCol == col:
                    continue  # current spot
                var neighbor = get_coord_data(row=dRow, col=dCol)
                if neighbor > spot:
                    cooler += 1

        if cooler > half_count:  # each spot has 8 neighbors
            print(t"Cool spot: ({row}, {col})")

The boundary check uses chained comparisons to make sure that at the current spot, there are at least radius values in each direction. This makes sure indexes in the following loop are safe.

Chained comparisons split pairwise: x < y <= z is equivalent to x < y and y <= z. They’re a compact Mojo way to express related checks.

Append the (row, col) tuple to cool_indices once you’ve confirmed a cool spot:

        if cooler > half_count:  # each spot has 8 neighbors
            print(t"Cool spot: ({row}, {col})")
            cool_indices.append((row, col))

When you run the search, you’ll find five cool spots: (2, 2), (2, 3), (2, 5), (3, 2), and (3, 3).

Takeaways

  • The outer loop visits every cell.
  • The inner loops visit each neighboring location.
  • The current cell is skipped.
  • cooler counts warmer neighbors for one location.

Visualize the answer

Finding the answer is good. Seeing it is even better.

Call write_data() one more time, now that the search has filled in cool_indices:

    write_data()

Takeaways

  • Store interesting coordinates separately instead of modifying the data.
  • The same display routine works before and after the search.

Run the app

The highlighted grid makes it clear where the colder regions are:

 -30  -25  -28  -32  -20  -15   -3
 -22  -18   -5  -20  -15  -10  -11
 -27  -20  -30* -22* -17  -18* -19
 -19   -3  -32* -31* -16  -10  -11
 -24  -30  -22  -30  -19   -9   -8

The failed warming pad is immediately obvious.

Final code

Your complete work_with_grids.mojo:

from std.pathlib import Path


def main() raises:
    var log = Path("src/downloads/grid_temps.txt")
    var lines = log.read_text().splitlines()

    var rows = len(lines)
    var cols = lines[0].byte_length() / 2
    var data = List[Int]()

    for line in lines:
        for i in range(0, line.byte_length(), 2):
            var pair = line[byte = i : i + 2]
            data.append(-Int(String(pair)))

    def index_to_coord(index: Int) {imm} -> Tuple[Int, Int]:
        return (index / cols, index % cols)

    def get_coord_data(row: Int, col: Int) {imm} -> Int:
        return data[row * cols + col]


    var cool_indices: List[Tuple[Int, Int]] = []

    # Write the data in a grid format
    def write_data() {imm data, imm cool_indices, imm}:
        var print_width = 4
        for row in range(rows):
            for idx in range(row * cols, (row + 1) * cols):
                var item = data[idx]
                var col = idx % cols
                print(t"{String(item).ascii_rjust(print_width)}", end="")
                if (row, col) in cool_indices:
                    print(t"*", end="")
                else:
                    print(t" ", end="")
            if row < rows - 1:
                print("")
        print()


    write_data()

    var radius = 1  # how far to look
    var count = (radius * 2 + 1) ** 2 - 1  # how many neighbors?
    var half_count = count / 2  # half of the neighbors

    for index in range(len(data)):
        var row, col = index_to_coord(index)  # fetch the row and column
        var cooler = 0  # keeps a running comparison count
        var spot = get_coord_data(row=row, col=col)  # the current spot's value

        # Check boundaries for safe indexing
        if not (
            radius <= row < rows - radius and radius <= col < cols - radius
        ):
            continue

        for dRow in range(row - radius, row + radius + 1):
            for dCol in range(col - radius, col + radius + 1):
                if dRow == row and dCol == col:
                    continue  # current spot
                var neighbor = get_coord_data(row=dRow, col=dCol)
                if neighbor > spot:
                    cooler += 1

        if cooler > half_count:  # each spot has 8 neighbors
            print(t"Cool spot: ({row}, {col})")
            cool_indices.append((row, col))

    write_data()

Topics covered

Representing a grid in a linear array, row-major indexing, tuple unpacking, nested loops, coordinate arithmetic, chained comparisons, neighborhood searches, and simple visualization.