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.
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 inmain(). Here it capturescolsanddataas 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.
Print the grid
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 populatescool_indicesis 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.
Clarifying the search
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.
Performing the search
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.
coolercounts 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.